Skip to content
Snippets Groups Projects
Commit 460545cd authored by Jan Schnathmeier's avatar Jan Schnathmeier
Browse files

Add Chapter Operations

parent 63cf46db
No related branches found
No related tags found
No related merge requests found
Showing
with 676 additions and 11 deletions
......@@ -8,4 +8,4 @@ An alternative method is to start with $\Phi(\mathcal{M})=\mathcal{B}$ since thi
\input{ch/EmbeddedIsotropicRemeshing/Algorithm.tex}
\input{ch/EmbeddedIsotropicRemeshing/CollapseOrder.tex}
\input{ch/EmbeddedIsotropicRemeshing/Smoothing.tex}
\input{ch/EmbeddedIsotropicRemeshing/Optimization.tex}
\ No newline at end of file
\input{ch/EmbeddedIsotropicRemeshing/ImplementationDetails.tex}
\ No newline at end of file
......@@ -45,7 +45,7 @@ Since collapsing too much at once introduces randomness into the result, there n
These simple locks add a wonderful property, in that there can be no chain of collapses (eg. collapse $v_a$ into $v_b$ and then $v_b$ into $v_c$) in a single iteration of \textsc{Collapse}. This also distributes the collapses quite evenly, retaining mesh structure. Even more, the total number of possible collapses per iteration is now limited to at most $\frac{1}{3}|V^{\mathcal{M}}|$, since the average valence is 6. Figure \ref{fig:HormannGrid} shows a mesh where the green vertices can be collapsed and the red vertices will be locked in turn; a configuration like that collapses the maximum of $\frac{1}{3}|V^{\mathcal{M}}|$ vertices. In practice, much less collapses happen since they are not distributed as evenly, and not every edge is short enough to be collapsed.
In our implementation, there are additional constraints to consider in order to optimize collapse order. Due to the nature of the embedding, it is sometimes necessary to split edges of $E^\mathcal{B}$ in order to make space to embed edges of $E^{\Phi(\mathcal{M})}$. These new vertices and edges of $\mathcal{B}$ are temporary and only implemented to guarantee connectivity, and whenever they are no longer needed they are deleted. From this perspective it makes sense to give embedded meta edges $e^{\mathcal{M}}_i\in E^{\Phi(\mathcal{M})}$ weights based on the non-original base edges it consists of: $e^{\mathcal{M}_i=\{e^{\mathcal{B}_i}\}, e^{\mathcal{B}_i}\in E^{\mathcal{B}}$. For this purpose we assign a weight function for base edges $e^{\mathcal{B}_i}$.
In our implementation, there are additional constraints to consider in order to optimize collapse order. Due to the nature of the embedding, it is sometimes necessary to split edges of $E^\mathcal{B}$ in order to make space to embed edges of $E^{\Phi(\mathcal{M})}$. These new vertices and edges of $\mathcal{B}$ are temporary and only implemented to guarantee connectivity, and whenever they are no longer needed they are deleted. From this perspective it makes sense to give embedded meta edges $e^{\mathcal{M}}_i\in E^{\Phi(\mathcal{M})}$ weights based on the non-original base edges it consists of: $e^{\mathcal{M}}_i=\{e^{\mathcal{B}_i}\}, e^{\mathcal{B}_i}\in E^{\mathcal{B}}$. For this purpose we assign a weight function for base edges $e^{\mathcal{B}_i}$.
\begin{equation}\label{eq:BaseEdgeWeight}
w_{\mathcal{B}}(e^{\mathcal{B}}_i) := \bigg\{\begin{array}{lr}
......@@ -63,7 +63,6 @@ In our implementation, there are additional constraints to consider in order to
\caption{Weights of new edges of $\mathcal{B}$ after splits.}
\label{fig:SplitWeights}
\end{figure}
\vspace{-10pt}
Where $E^{\mathcal{B}'}$ denotes the \textit{original} edges of $\mathcal{B}$ and $anc(e^{\mathcal{B}}_i)$ returns the \textit{ancestor} of $e^{\mathcal{B}}_i$, meaning the edge $e^{\mathcal{B}}_i$ was split off from. In this way heavily split up areas of $\mathcal{B}$ exponentially increase in weight marking the need for a cleanup. Figure \ref{fig:SplitWeights} visualizes how this works. Part (a) shows two original faces of $\mathcal{B}$, where all edges have weight 1. In part (b), the central edge is split. The two edges representing the original edge retain weight 1, whereas the two new edges (yellow) double their weight to two. Finally, (c) shows a split of a yellow edge where the newly created edges (red) again double their weight to four. This type of edge weighting heavily favors original edges of $\mathcal{B}$, and also provides a data structure to roll back changes by collapsing away non-original edges.
......@@ -89,11 +88,11 @@ Using these weights, we observe that base edges of high weight cluster around me
\label{fig:CollapseHeuristic}
\end{wrapfigure}
Figure \ref{fig:CollapseHeuristic} visualizes the edges changed by collapsing the halfedge $h^{\mathcal{M}}_x$ from $v^{\mathcal{M}}_A$ to $v^{\mathcal{M}}_B$. Halfedges $h^{\mathcal{M}}_p$ and $h^{\mathcal{M}}_{on}$ exist only before the collapse, so their weight can be discounted. $h^{\mathcal{M}}_x$ entering vertex $v^{\mathcal{M}}_B$ is replaced by $|v^{\mathcal{M}}_A|-3$ halfedges entering $v^{\mathcal{M}}_B$, and the topology around the vertices they come from does not change. We estimate that each of these edges increases in weight by $\frac{1}{2}w_{\mathcal{M}}(h^{\mathcal{M}}_x)$, representing half of the weight of $h^{\mathcal{M}}_x$, gained while entering $v^{\mathcal{M}}_B$. Thus we derive the following collapse heuristic:
Figure \ref{fig:CollapseHeuristic} visualizes the edges changed by collapsing the halfedge $h^{\mathcal{M}}_x$ from $v^{\mathcal{M}}_A$ to $v^{\mathcal{M}}_B$. Halfedges $h^{\mathcal{M}}_p$ and $h^{\mathcal{M}}_{on}$ exist only before the collapse, so their weight can be discounted. $h^{\mathcal{M}}_x$ entering vertex $v^{\mathcal{M}}_B$ is replaced by $|v^{\mathcal{M}}_A|-3$ halfedges entering $v^{\mathcal{M}}_B$, and the topology around the vertices they come from does not change. We estimate that each of these edges increases in weight by $w_{\mathcal{M}}(h^{\mathcal{M}}_x)$,. Thus we derive the following collapse heuristic:
\vspace{-10pt}
\begin{equation}\label{eq:CollapseHeuristic}
\textsc{ch}(h^{\mathcal{M}}_x) :=\frac{1}{2} (|v^{\mathcal{M}}_A|-4)*w_{\mathcal{M}}(h^{\mathcal{M}}_x) - w_{\mathcal{M}}(h^{\mathcal{M}}_p) - w_{\mathcal{M}}(h^{\mathcal{M}}_{on})
\textsc{ch}(h^{\mathcal{M}}_x) := (|v^{\mathcal{M}}_A|-4)*w_{\mathcal{M}}(h^{\mathcal{M}}_x) - w_{\mathcal{M}}(h^{\mathcal{M}}_p) - w_{\mathcal{M}}(h^{\mathcal{M}}_{on})
\end{equation}
Where $v^{\mathcal{M}}_A$ is the \textit{from\_vertex} and $v^{\mathcal{M}}_B$ is the \textit{to\_vertex} of $h^{\mathcal{M}}_x$, and $h^{\mathcal{M}}_p, h^{\mathcal{M}}_{on}$ are the previous halfedge and opposite next halfedge of $h^{\mathcal{M}}_x$ respectively. Using this heuristic Equation \ref{eq:CollapseHeuristic}, we sort all meta edges from low to high values and check low value halfedges for collapses first. Given the properties optimized by this heuristic it should perform better than collapsing based on a randomized set of edges, and a detailed comparison can be found in Chapter \ref{ch:Evaluation}.
......@@ -102,7 +101,7 @@ Where $v^{\mathcal{M}}_A$ is the \textit{from\_vertex} and $v^{\mathcal{M}}_B$ i
\vspace{-10pt}
\begin{align}\label{eq:CollapseHeuristicNonTriangles}
\textsc{ch}'(h^{\mathcal{M}}_x) := \frac{1}{2}(|v^{\mathcal{M}}_A|-2-\textsc{tr}(h^{\mathcal{M}}_x)-\textsc{tr}(h^{\mathcal{M}}_o))*w_{\mathcal{M}}(h^{\mathcal{M}}_x) \\ \nonumber
\textsc{ch}'(h^{\mathcal{M}}_x) := (|v^{\mathcal{M}}_A|-2-\textsc{tr}(h^{\mathcal{M}}_x)-\textsc{tr}(h^{\mathcal{M}}_o))*w_{\mathcal{M}}(h^{\mathcal{M}}_x) \\ \nonumber
- (\textsc{tr}(h^{\mathcal{M}}_x)*w_{\mathcal{M}}(h^{\mathcal{M}}_p)) - (\textsc{tr}(h^{\mathcal{M}}_o)*w_{\mathcal{M}}(h^{\mathcal{M}}_{on}))
\end{align}
......
\section{Optimization}
\label{sec:Optimization}
\section{Implementation Details}
\label{sec:ImplementationDetails}
\begin{wrapfigure}[16]{r}{0.55\textwidth}
\vspace{-20pt}
......@@ -9,9 +9,10 @@
\caption{Ugly artifacts on $\mathcal{B}$}
\label{fig:extremeartifacts}
\end{wrapfigure}
\vspace{-5pt}
Now that all the components of Embedded Remeshing have been presented, a few more things have to be said regarding optimizing the algorithm, and perhaps algorithms on embedded meta meshes in general.
\begin{itemize}
\item \textbf{High valence vertices}: Creating vertices of high valence is costly, since connectivity of new edges $e^{\mathcal{M}}_i$ is often only possible via performing splits on $\mathcal{B}$. This effect cascades as more and more edges are connected to a vertex. As the amount of free space in a patch near a vertex decreases, the length of edges created by splits decreases as well, and each new meta edge requires a multiple of the splits the previous one required.\footnote{In development there were cases where inefficient implementations increased the number of vertices in a mesh by a factor of 1000 or more.} This makes a careful implementation with respect to meta vertices very important. Figure \ref{fig:extremeartifacts} shows an extreme example, but errors like that are common when using an unoptimized implementation and can slow algorithms down considerably.
\item \textbf{High valence vertices}: Creating vertices of high valence is costly, since connectivity of new edges $e^{\mathcal{M}}_i$ is often only possible via performing splits on $\mathcal{B}$. This effect cascades as more and more edges are connected to a vertex. As the amount of free space in a patch near a vertex decreases, the length of edges created by splits decreases as well, and each new meta edge requires a multiple of the splits the previous one required.\footnote{In development there were cases where inefficient implementations increased the number of vertices in a mesh by a factor of 1000 or more.} This makes a careful implementation with respect to meta vertices very important. Figure \ref{fig:extremeartifacts} and Figure \ref{fig:ExtremeSpital} show extreme examples making it clear why optimization is vital.
\end{itemize}
In some cases this effect is especially pronounced and forms spirals, similar to the swirls observed by Praun et al. \cite{praun2001consistent}
\begin{itemize}
......@@ -19,3 +20,12 @@ In some cases this effect is especially pronounced and forms spirals, similar to
\item \textbf{Cleanup}: Tracing into and deleting meta edges from the base mesh creates continually creates and deletes elements in the base mesh $\mathcal{B}$. The way in which edge cleanup and garbage collection are used on $\mathcal{B}$ can have a strong impact on performance.
\end{itemize}
Ultimately, algorithms on our data structure require careful planning given a dense meta mesh. On sparse meta meshes (compared to the underlying base mesh) these problems do not occur much, since there is sufficient room for tracing without changing the underlying base mesh.
\vspace{-20pt}
\begin{figure}%[htb]
\begin{centering}
\includegraphics[width=\textwidth]{img/extremespiral.png}
\end{centering}
\vspace{-20pt}
\caption{Extreme spiraling artifacts (created deliberately to visually pronounce the effect).}
\label{fig:ExtremeSpital}
\end{figure}
\chapter{Operations}
\label{ch:Operations}
This chapter is dedicated to explaining implementation details regarding the operations of our meta mesh embedding. These can be roughly split into three parts:
\begin{enumerate}
\item Section \ref{sec:MetaMeshUpdates}: Operations that update the topology of the meta mesh $\mathcal{M}$ such as edge rotation, halfedge collapse\dots
\item Section \ref{sec:EmbeddingUpdates}: Operations that only update the embedding $\Phi(\mathcal{M})$ while preserving the topology of $\mathcal{M}$, such as vertex relocation or retracing an edge.
\item Section \ref{sec:BaseMeshOperations}: Sub-operations, performed on the base mesh $\mathcal{B}$, which are required components of operations on the mesh embedding $\Phi(\mathcal{M})$. This includes tracing edges in $E^{\Phi(\mathcal{M})}$ on $\mathcal{B}$ as well as refining $\mathcal{B}$.
\end{enumerate}
\input{ch/Operations/MetaMeshUpdates.tex}
\input{ch/Operations/EmbeddingUpdates.tex}
\input{ch/Operations/BaseMeshOperations.tex}
\section{Base Mesh Operations}
\label{sec:BaseMeshOperations}
The last category of operations we introduce are \textit{base mesh operations}. These are in a different category than the methods introduced in Sections \ref{sec:MetaMeshUpdates} and \ref{sec:EmbeddingUpdates} as they work on a much lower level. Base mesh operations are essential low level building blocks that are required to enable the higher level mesh operations previously presented in this chapter. They include the minimal and complete set of operations from \cite{akleman2003minimal}:
\begin{itemize}
\item \textit{Vertex deletion from $\Phi(\mathcal{M})$}: Delete a vertex $v^\mathcal{M}_A$ as well as its embedding $v^{\Phi(\mathcal{M})}_A$.
\item \textit{Edge deletion from $\Phi(\mathcal{M})$}: Delete an edge $e^\mathcal{M}_A$ as well as its embedding $e^{\Phi(\mathcal{M})}_A$.
\item \textit{Vertex insertion into $\Phi(\mathcal{M})$}: Map a base vertex $v^\mathcal{B}_A$ to a meta vertex $v^\mathcal{M}_B$ such that $v^{\Phi(\mathcal{M})}_{B}=v^\mathcal{B}_A$.
\end{itemize}
These are quite trivial to implement and do not require any further elaboration. However there is one base mesh operation that is much more complex and can vary greatly depending on its implementation, and that is:
\begin{itemize}
\item \textit{Edge insertion into $\Phi(\mathcal{M})$}: Given an edge $e^\mathcal{M}_A$ that is to be inserted into $\Phi(\mathcal{M})$, it is non-trivial to trace an embedded edge $e^{\Phi(\mathcal{M})}_A$. Section \ref{subsec:RestrictedPathTracing} elaborates on this.
\end{itemize}
Separate from these operations, there is one more base mesh operation which facilitates edge tracing, namely:
\begin{itemize}
\item \textit{Base mesh refinement}: When tracing embedded edges $e^{\Phi(\mathcal{M})}_i$, it is greatly beneficial to perform some refinement on $\mathcal{B}$ beforehand. This refinement is done to ensure in-sector connectivity between all vertices on the border of the sector that the trace is being performed in.\footnote{Callback to Figure \ref{fig:tracingspaceproblem} in Section \ref{subsec:PathEmbeddings}.}
\end{itemize}
With that said, this section discusses restricted path tracing in Section \ref{subsec:RestrictedPathTracing} and base mesh refinement in Section \ref{subsec:BaseMeshRefinement}.
\input{ch/Operations/BaseMeshOperations/RestrictedPathTracing.tex}
\input{ch/Operations/BaseMeshOperations/BaseMeshRefinement.tex}
\subsection{Base Mesh Refinement}
\label{subsec:BaseMeshRefinement}
The previous Section \ref{subsec:RestrictedPathTracing} introduced restricted path tracing; bidirectional A* tracing restricted to patches delimited by embedded meta edges $e^{\Phi(\mathcal{M})}_i$. It can happen that there is no consecutive path of edges $\{e^{\mathcal{B}}_{A,B},e^{\mathcal{B}}_{B,C},\dots, e^{\mathcal{B}}_{N,M}\}$ to represent an embedded edge $e^{\Phi(\mathcal{M})}_{A,M}$ given a set of restrictions $R^\mathcal{B}\subset E^\mathcal{B}$; formally:
\begin{align}
\nexists e^{\Phi(\mathcal{M})}_{A,M}:=\{e^{\mathcal{B}}_{A,B},e^{\mathcal{B}}_{B,C},\dots, e^{\mathcal{B}}_{N,M}\}
\land \forall e^{\mathcal{B}}_{i}\in e^{\Phi(\mathcal{M})}_{A,M}, e^{\mathcal{B}}_{i}\notin R^\mathcal{B}
\end{align}
In such a case our tracing method fails to embed the edge $e^{\Phi(\mathcal{M})}_{A,M}$, so naturally this should be avoided. The set of restrictions $R^\mathcal{B}_E$ is given by the embedded edges, meaning:
\begin{align}
R^\mathcal{B}_E := \{e^{\mathcal{B}}_{i}\in E^\mathcal{B} | e^{\mathcal{B}}_{i}\in\bigcup_{e^{\Phi(\mathcal{M})}_j \in E^{\Phi(\mathcal{M})}} e^{\Phi(\mathcal{M})}_j\}
\end{align}
During initial triangulation an extended set of restrictions is considered, including voronoi border edges as restrictions $R^{\mathcal{B}}_V$:
\begin{align}
R^\mathcal{B}_V := \{e^{\mathcal{B}}_{A,B}\in E^\mathcal{B} | \textsc{voronoi}(v^{\mathcal{B}}_A) \neq \textsc{voronoi}(v^{\mathcal{B}}_B)\}
\end{align}
Resulting in a total set of restrictions $R^\mathcal{B}_T=R^\mathcal{B}_E\cup R^\mathcal{B}_V$.
\begin{figure}[ht]
\def\svgwidth{\textwidth}
{\centering
\input{img/PreProcessing.pdf_tex}\par
}
\vspace{-1pt}
\caption{Base mesh refinement: edges $e^{\mathcal{B}}_r\in R^\mathcal{B}$ in red, edges $e^{\mathcal{B}}_i$ that need to be split in teal. (a) pre-refinement (b) post-refinement}
\label{fig:PreProcessing}
\end{figure}
In order to enable tracing in patches of $\mathcal{B}$ we facilitate connectivity between all elements of a patch restricted by $R^\mathcal{B}_E$ - or $R^\mathcal{B}_T$ during initial triangulation; from here on write $R^\mathcal{B}$ to cover both cases. This can be done by refining the underlying base mesh through a series of splits, until full connectivity is reached. We observe, that whenever connectivity is limited, there exists a base halfedge $e^\mathcal{B}_{X,Y}$ with vertices $v^\mathcal{B}_X$ and $v^\mathcal{B}_Y$ both lying on restrictions; see teal edges in Figure \ref{fig:PreProcessing}.
After every edge $e^\mathcal{B}_{X,Y}$ with $v^\mathcal{B}_X, v^\mathcal{B}_Y$ lying on edges of $R^\mathcal{B}$ has been split, every pair of vertices inside the patch can be connected, as can be seen in Figure \ref{fig:PreProcessing}.(b). Since every edge $e^\mathcal{B}_{i}$ which requires splitting is adjavent to an edge $e^\mathcal{B}_{j}$, we can refine parts of $\mathcal{B}$ locally instead of having to iterate over all edges in $E^\mathcal{B}$. Before we trace an embedded meta edge $e^{\Phi(\mathcal{M})}_i$, we apply the following refinement algorithm:
\begin{algorithm}[H]
\caption{Patch pre-processing}\label{alg:PreProcessPatch}
\begin{algorithmic}[1]
\Function{PreProcessPatch}{$e^{\Phi(\mathcal{M})}_x$}
\For {$h^{\Phi(\mathcal{M})}_i\in \textsc{Patch}(e^{\Phi(\mathcal{M})}_x)$}\label{alg:PreProcessPatchLoop1}
\For {$h^{\mathcal{B}}_j\in h^{\Phi(\mathcal{M})}_i$}\label{alg:PreProcessPatchLoop2}
\For {$h^{\mathcal{B}}_{X,Y}\in \textsc{LeftHalfCircle}(h^{\Phi(\mathcal{M})}_i)$}\label{alg:PreProcessPatchLoop3}
\If {$v^\mathcal{B}_Y\in R^\mathcal{B}$}
\State $\textsc{Split}(h^{\mathcal{B}}_{X,Y})$
\EndIf
\EndFor
\EndFor
\EndFor
\EndFunction
\end{algorithmic}
\end{algorithm}
\begin{wrapfigure}[21]{r}{0.50\textwidth}
\vspace{-30pt}
\def\svgwidth{0.5\textwidth}
{\centering
\input{img/CollapseStressTest.pdf_tex}\par
}
\vspace{-1pt}
\caption{Damaged mesh $\mathcal{B}$ after stress test vs original state $\mathcal{B}'$ side by side.}
\label{fig:CollapseStressTest}
\end{wrapfigure}
Line \ref{alg:PreProcessPatchLoop1} of Algorithm \ref{alg:PreProcessPatch} loops over the halfedges $h^{\Phi(\mathcal{M})}_i$ of the patch of $e^{\Phi(\mathcal{M})}_x$; these are the embedded \textit{inner} halfedges in $\Phi(\mathcal{M})$. The function \textsc{LeftHalfCircle}$(h^{\mathcal{B}}_{X,Y})$ returns all halfedges not in $R^\mathcal{B}$ traversed by circulating outgoing halfedges of $v^\mathcal{B}_X$ until a halfedge in $R^\mathcal{B}$ is reached. Thus, the only edges that are considered for a split are exactly the edges inside the patch of $e^{\Phi(\mathcal{M})}_x$ which touch the patch border.
With that, pre-processing before edge tracing is well defined; however it is vital to keep in mind that splits on $E^\mathcal{B}$ change the underlying base mesh $\mathcal{B}$. Proceeding with this refinement without post-processing to clean up $\mathcal{B}$ can lead to very damaged base meshes. Figure \ref{fig:CollapseStressTest} shows such an example; a mesh that was deliberately damaged by running a collapse stress test on $\mathcal{M}$ and not including any post-processing. This manifests in areas of $\mathcal{B}$ becoming extremely dense, since embedding meta edges into already split areas results in \textit{even more} splits in later traversals.
The damage caused on the mesh in Figure \ref{fig:CollapseStressTest} alone should be reason enough to find a way of post-processing that avoids this. Ideally, non-original edges in $E_\mathcal{B}$ should only exist as long as they need to, and then be collapsed. Much like we defined a pre-processing algorithm for a patch \textit{before} tracing an edge, we also define a post-processing algorithm to undo all changes that can be undone \textit{after} a trace completes and every time an edge $e^{\Phi(\mathcal{M})}_i\in E^{\Phi(\mathcal{M})}$ is removed from the embedding $\Phi(\mathcal{M})$.
\begin{algorithm}[H]
\caption{Patch post-processing}\label{alg:PostProcessPatch}
\begin{algorithmic}[1]
\Function{PostProcessPatch}{$e^{\Phi(\mathcal{M})}_x$}
\For {$h^{\Phi(\mathcal{M})}_i\in \textsc{Patch}(e^{\Phi(\mathcal{M})}_x)\cup h^{\Phi(\mathcal{M})}_{x_1}\cup h^{\Phi(\mathcal{M})}_{x_2}$}\label{alg:PostProcessPatchLoop1}
\For {$h^{\mathcal{B}}_j\in h^{\Phi(\mathcal{M})}_i$}\label{alg:PostProcessPatchLoop2}
\For {$h^{\mathcal{B}}_{X,Y}\in \textsc{LeftHalfCircle}(h^{\Phi(\mathcal{M})}_i)$}\label{alg:PostProcessPatchLoop3}
\If {$v^{\mathcal{B}}_{X}\notin V^\mathcal{B'}\land h^{\mathcal{B}}_{X,Y}\notin H^\mathcal{B'}$}
\State $\textsc{ConditionalMerge}(h^{\mathcal{B}}_{X,Y})$
\EndIf
\EndFor
\EndFor
\EndFor
\EndFunction
\end{algorithmic}
\end{algorithm}
Edge traversal works in the same way for Algorithms \ref{alg:PreProcessPatch} and \ref{alg:PostProcessPatch}, with the exception that Algorithm \ref{alg:PostProcessPatch} iterates over the newly traced edge $e^{\Phi(\mathcal{M})}_x$ as well as denoted by adding $h^{\Phi(\mathcal{M})}_{x_1}\cup h^{\Phi(\mathcal{M})}_{x_2}$ to Line \ref{alg:PostProcessPatchLoop1}. $V^\mathcal{B'}, H^\mathcal{B'}$ denote the \textit{original} halfedges and vertices of $\mathcal{B}$; we want to collapse non-original vertices along non-original halfedges. For those halfedges we check if they should be collapsed:
\begin{algorithm}[H]
\caption{Conditional merging}\label{alg:ConditionalMerge}
\begin{algorithmic}[1]
\Function{ConditionalMerge}{$h^{\mathcal{B}}_{X,Y}$}
\If {$v^\mathcal{B}_X\in V^{\Phi(\mathcal{M})}$}\label{alg:ConditionalMergeCond1}
\State\Return{}
\EndIf
\If {$(v^\mathcal{B}_Y\in V^{\Phi(\mathcal{M})}\land\neg\textsc{MetaEdge}(h^\mathcal{B}_{X,Y})\land\textsc{MetaEdge}(v^\mathcal{B}_{X}))$}\label{alg:ConditionalMergeCond2}
\State\Return{}
\EndIf
\If {$-1\neq\textsc{MetaEdge}(v^\mathcal{B}_X)\neq\textsc{MetaEdge}(v^\mathcal{B}_{Y})\neq-1$}\label{alg:ConditionalMergeCond3}
\State\Return{}
\EndIf
\State $\textsc{Collapse}(h^{\mathcal{B}}_{X,Y})$\label{alg:ConditionalMergeFin}
\EndFunction
\end{algorithmic}
\end{algorithm}
Where \textsc{MetaEdge}($v^\mathcal{B}_x$)/\textsc{MetaEdge}($h^\mathcal{B}_y$) returns the meta edge that $v^\mathcal{B}_x$ or $h^\mathcal{B}_y$ lies on, and $-1$ if it does not lie on a meta edge. Step by step:
\begin{itemize}
\item \textbf{Line \ref{alg:ConditionalMergeCond1}}: Check if the \textit{from\_vertex} $v^\mathcal{B}_X$ of $h^{\mathcal{B}}_{X,Y}$ is an embedded meta vertex, we disallow collapsing in those cases for stability.
\item \textbf{Line \ref{alg:ConditionalMergeCond2}}: Check if the \textit{to\_vertex} $v^\mathcal{B}_Y$ of $h^{\mathcal{B}}_{X,Y}$ is an embedded meta vertex, $h^{\mathcal{B}}_{X,Y}$ does \textit{not} lie on a meta edge, and $v^\mathcal{B}_X$ does lie on a meta edge. If those conditions are true, collapsing $h^{\mathcal{B}}_{X,Y}$ would merge a meta edge with a meta vertex it does not belong to, so a collapse cannot happen.
\item \textbf{Line \ref{alg:ConditionalMergeCond3}}: Check if both vertices $v^\mathcal{B}_X$ and $v^\mathcal{B}_Y$ lie on distinct meta edges. In that case collapsing $h^{\mathcal{B}}_{X,Y}$ would merge two separate meta edges, so it cannot be permitted.
\item \textbf{Line \ref{alg:ConditionalMergeFin}}: If none of the above conditions apply, $h^{\mathcal{B}}_{X,Y}$ is collapsed.
\end{itemize}
Using the post-processing Algorithm \ref{alg:PostProcessPatch} after every edge trace, and a similar method accompanying every embedded edge deletion, the base mesh $\mathcal{B}$ is kept clean of superfluous non-original elements - meaning after every operation $\mathcal{B}$ is as close to $\mathcal{B}'$ as possible without merging elements of the embedding $\Phi(\mathcal{M})$. We also provide global pre- and post-processing methods from a previous implementation, but local methods are always preferable since they scale better.
\subsection{Restricted Path Tracing}
\label{subsec:RestrictedPathTracing}
\begin{wrapfigure}[16]{r}{0.50\textwidth}
\vspace{-35pt}
\def\svgwidth{0.5\textwidth}
{\centering
\input{img/CatEdges.pdf_tex}\par
}
\vspace{-1pt}
\caption{Embedded edges $e^{\Phi(\mathcal{M})}_{i}\in E^{\Phi(\mathcal{M})}$ with corresponding meta edges $e^{\mathcal{M}}_i\in E^{\mathcal{M}}$ overlayed.}
\label{fig:CatEdges}
\end{wrapfigure}
Meta edges $e^{\mathcal{M}}_i\in E^{\mathcal{M}}$ are embedded into the base mesh $\mathcal{B}$ as consecutive chains of base edges such that $e^{\Phi(\mathcal{M})}_{i}:=\{e^{\mathcal{B}}_1, e^{\mathcal{B}}_2, \dots, e^{\mathcal{B}}_n\}$. Figure \ref{fig:CatEdges} visualizes such an embedding in practice, here embedded edges $e^{\Phi(\mathcal{M})}_{i}$ are drawn into an underlying base mesh $\mathcal{B}$ in colors, and the corresponding meta edges $e^{\mathcal{M}}_i$ are overlayed and colored in the same way.
When finding an embedding for an edge $e^{\mathcal{M}}_i$, the embedding should generally be as short as possible for obvious reasons\footnote{Much like in standard meshes edges are direct lines between vertices - the shortest path.}. Formally speaking, in order to embed an edge $e^{\mathcal{M}}_{A,B}$ from vertices $v^{\mathcal{M}}_A$ to $v^{\mathcal{M}}_B$ into $\mathcal{B}$, we are trying to minimize:
\vspace{-15pt}
\begin{align}\label{eq:MinimalPaths}
e^{\Phi(\mathcal{M})}_{A,B}:= \{e^{\mathcal{B}}_{A,C}, e^{\mathcal{B}}_{C,D}, \dots, e^{\mathcal{B}}_{N,B}\},
v^{\mathcal{B}}_A = v^{\Phi(\mathcal{M})}_A,
v^{\mathcal{B}}_B = v^{\Phi(\mathcal{M})}_B \\
|e^{\Phi(\mathcal{M})}_{A,B}|:=\sum_{e^{\mathcal{B}}_i\in e^{\Phi(\mathcal{M})}_{A,B}}(|e^{\mathcal{B}}_i|)\\
min_{e^{\Phi(\mathcal{M})}_{A,B}}(|e^{\Phi(\mathcal{M})}_{A,B}|)
\end{align}
With an edge in the form $e_{X,Y}$ denoting $X$ as its \textit{from\_vertex} and $Y$ as its \textit{to\_vertex}. In other words we are trying to find the edge embedding $e^{\Phi(\mathcal{M})}_{A,B}$ for which the length $|e^{\Phi(\mathcal{M})}_{A,B}|$ is minimal.
Finding a minimal path between two vertices $v_A$ and $v_B$ on a graph is a well-established problem, which has been solved previously by Dijkstra's Algorithm \cite{dijkstra1959note}, which basically performs a breadth-search from $v_A$ until $v_B$ is reached. This can be improved by, instead, performing two breadth-searches from $v_A$ and $v_B$ until they meet somewhere in the middle. Bidirectional search is more efficient since instead of growing one circle with a radius equaling the distance between $v_A$ and $v_B$, two circles are grown with half the radius. The surface of those two circles is lower, and thus the search has to step over less vertices.
A further improvement is to introduce heuristics into Dijkstra's Algorithm, which the algorithm known as A* \cite{hart1968formal} does. The surface-distance between two points $v_A$ and $v_B$ on the surface of a mesh is measured as the sum of the edge lengths of the shortest edge chain between $v_A$ and $v_B$. However in most cases, the direct distance between $v_A$ and $v_B$ is a good heuristic approcimating the surface-distance. For bidirectional A* between vertices $v_A$ and $v_B$, we require two potential functions $\pi_A(v_x), \pi_B(v_x)$ which estimate the distances between $v_A$ and $v_x$ as well as $v_B$ and $v_x$ respectively. These potential functions are called \textit{consistent} if and only if:
\begin{align}
\pi_A(v_x)+\pi_B(v_x)=\pi_A(v_y)+\pi_B(v_y); \forall v_y,v_x\in V
\end{align}
$\pi_A(v_x)$ and $\pi_B(v_x)$ are clearly not consistent potential functions, but their average is a consistent potential function \cite{ikeda1994fast}. Thus we derive potential functions:
\begin{align}
p_A(v_x) = \frac{1}{2}(\pi_A(v_x)-\pi_B(v_x))+\frac{\pi_B(A)}{2}\\
p_B(v_x) = \frac{1}{2}(\pi_B(v_x)-\pi_A(v_x))+\frac{\pi_A(B)}{2}
\end{align}
Note that the terms $\frac{\pi_B(A)}{2}$ and $\frac{\pi_A(B)}{2}$ are constant, and only there to ensure that $p_A(A)=0$ and $p_B(B)=0$. The vertices $v_i$ are then sorted according to path lengths and heuristics $p_A(v_X), p_B(v_x)$ into two minheaps, and A* is performed with a few restrictions.
We want to restrict A* to only find paths inside sectors enclosed by embedded meta edges $e^{\Phi(\mathcal{M})}_i$, that way edges cannot overlap, and cyclic edge order can be preserved. This is achieved during by excluding certain vertices when searching for neighbors. Whenever a vertex $v^\mathcal{B}_j$ is on top of the heap during A*, its outgoing halfedges are added onto the heap with the distance value of $v^\mathcal{B}_j$ plus the edge length to $v^\mathcal{B}_j$ and adjusted by our heuristics. The restriction comes into place here; whenever a neighboring vertex of $v^\mathcal{B}_j$ lies on a meta edge or meta vertex, that neighbor is not considered. Furthermore, during initial triangulation, neighbors lying on voronoi borders except the one corresponding to the edge that is being traced are also not considered.
These restrictions ensure that a trace stays inside its sector, so it is also important to ensure that the trace starts inisde the correct sector (or the two traces won't connect). This is done by circulating over the outgoing meta and base halfedges of the start and end vertices $v^\mathcal{B}_x$, $v^\mathcal{B}_y$ of the edge to be trace $e^{\Phi(\mathcal{M})}_{x,y}$, and finding outgoing base halfedges that respect cyclical edge order. Tracing is then performed only in the sectors these halfedges point to.
\begin{figure}[ht]
\def\svgwidth{\textwidth}
{\centering
\input{img/TracingVisualized.pdf_tex}\par
}
\vspace{-1pt}
\caption{Bidirectional A* tracing of (a) edge $e^{\Phi(\mathcal{M})}_{B,C}$ during initial triangulation (b) edge $e^{\Phi(\mathcal{M})}_{A,D}$ after a flip. The base halfedges marked in red were traversed in A*.}
\label{fig:TracingVisualized}
\end{figure}
Figure \ref{fig:TracingVisualized} visualizes the edges of $\mathcal{B}$ traversed during A* edge tracing, by marking all base halfedges that were traversed by A* in red.
\begin{itemize}
\item \textbf{(a)}: Here, the edge $e^{\Phi(\mathcal{M})}_{B,C}$ (brown) was just traced during the \textit{initial triangulation} of $\Phi(\mathcal{M})$. During initial triangulation, traces are restricted to the voronoi regions of the two vertices they connect, and only allowed to traverse the single voronoi border corresponding to the edge. In this case, the traces traversed the entire parts of their respective voronoi regions in the tracing sector, since the shape enclosing the traced base edges is rectangular and not round.
Thus, the restriction towards voronoi regions during initial triangulation not only ensures proper topology, but also makes the trace operation slightly faster (since there are less vertices that can be legally traversed by A*). In the middle of $e^{\Phi(\mathcal{M})}_{B,C}$ a few base edges can be seen where both halfedges were selected, meaning the trace reached those edges from both sides. $e^{\Phi(\mathcal{M})}_{B,C}$ itself was then drawn into $\mathcal{B}$ based on the shortest path found by A*.
\item \textbf{(b)}: The previous edge $e^{\Phi(\mathcal{M})}_{B,C}$ was flipped, and is now $e^{\Phi(\mathcal{M})}_{A,D}$. Since the initial triangulation is now over, A* can traverse over the entirety of the sector spanned by $\{e^{\Phi(\mathcal{M})}_{A,B},e^{\Phi(\mathcal{M})}_{B,C},e^{\Phi(\mathcal{M})}_{C,D},e^{\Phi(\mathcal{M})}_{D,A}\}$. This manifests in the areas traversed resembling quarter-circles around $v^{\Phi(\mathcal{M})}_A$ and $v^{\Phi(\mathcal{M})}_D$.
\end{itemize}
\section{Embedding Updates}
\label{sec:EmbeddingUpdates}
The second class of operations on our embedding we consider are operations which change the embedding $\Phi(\mathcal{M})$ without changing the topology of the underlying meta mesh $\mathcal{M}$.
\input{ch/Operations/EmbeddingUpdates/VertexRelocation.tex}
\input{ch/Operations/EmbeddingUpdates/Retracing.tex}
\subsection{Retracing}
\label{subsec:Retracing}
Another method that updates $\Phi(\mathcal{M})$ but not $\mathcal{M}$ is retracing. Retracing a halfedge $h^{\Phi(\mathcal{M})}_i$ is simple; remove it from $\Phi(\mathcal{M})$ then trace it into $\Phi(\mathcal{M})$ again. By removing $h^{\Phi(\mathcal{M})}_i$, the two adjacent faces $f^{\Phi(\mathcal{M})}_1$ and $f^{\Phi(\mathcal{M})}_2$ merge into a simple patch $p^{\Phi(\mathcal{M})}_{f1|f2}$ as proved previously in Section \ref{subsec:HalfedgeCollapse} on halfedge collapses. Since $p^{\Phi(\mathcal{M})}_{f1|f2}$ is always simple, retracing cannot fail as long as the path tracing operation considers entry and exit sectors.
Retracing is useful because unlike in regular meshes, edges $e^{\Phi(\mathcal{M})}_i\in E^{\Phi(\mathcal{M})}$ are not always straight. Edges $e^{\Phi(\mathcal{M})}_i$ are traced along the shortest path \textit{available at the time they are traced}, meaning the optimal path for an edge $e^{\Phi(\mathcal{M})}_i$ can change as other edges change. Thus, retracing can be used to straighten edges; for example like we use it at the end of a collapse operation to straighten the edges in reverse order.
Another application is patchwise retracing. In our embedded remeshing algorithm this is used to decrease the number of elements of $\mathcal{B}$ by first removing all embedded edges $e^{\Phi(\mathcal{M})}_i$ of a patch $p^{\Phi(\mathcal{M})}_j$, collapsing all non-original edges in the patch in $\mathcal{B}$, then retracing the edges $e^{\Phi(\mathcal{M})}_i$. However note that this is more complicated as tracing order needs to be considered when retracing entire patches; it is best not to use this method on patches with genus $>$ 1.
\ No newline at end of file
\subsection{Vertex Relocation}
\label{subsec:VertexRelocation}
\begin{figure}[ht]
\begin{centering}
\begin{subfigure}{.5\textwidth}
\def\svgwidth{0.9\linewidth}
{\centering
\input{img/RelocateEdgeSplit.pdf_tex}\par
}
\vspace{-1pt}
\caption[width=.9\linewidth]{Vertex relocation from $v^{\Phi(\mathcal{M})}_A$ to $v^{\Phi(\mathcal{M})}_B$ on an edge of $E^{\Phi(\mathcal{M})}$.}
\label{fig:relocedgesplit}
\end{subfigure}
\begin{subfigure}{.5\textwidth}
\def\svgwidth{0.9\linewidth}
{\centering
\input{img/RelocateFaceSplit.pdf_tex}\par
}
\vspace{-1pt}
\caption[width=.9\linewidth]{Vertex relocation from $v^{\Phi(\mathcal{M})}_A$ to $v^{\Phi(\mathcal{M})}_B$ on a face of $F^{\Phi(\mathcal{M})}$.}
\label{fig:relocfacesplit}
\end{subfigure}
\end{centering}
\caption{Vertex relocation as a concatenation of a split and a collapse.}
\label{fig:relocwrapper}
\end{figure}
The first operation which updates $\Phi(\mathcal{M})$ without changing the topology of $\mathcal{M}$ is vertex relocation. This operation is performed by moving a vertex $v^{\Phi(\mathcal{M})}_A$ into a position in an adjacent face or edge, so it doesn't change the connectivity of $\mathcal{M}$. However, the edges $e^{\Phi(\mathcal{M})}_{A,i}$ of $v^{\Phi(\mathcal{M})}_A$ have to be retraced, and, as stated previously in Section \ref{subsec:HalfedgeCollapse}, the order in which edges are retraced matters if the patch of surrounding faces of $v^{\Phi(\mathcal{M})}_A$ does not have disc topology.
Thus it is convenient to define vertex relocation as a concatenation of a split and a collapse, thus alleviating the need for a complex implementation. A vertex relocation would then look as shown in Figure \ref{fig:relocedgesplit} for relocation into an edge and Figure \ref{fig:relocfacesplit} for relocation into a face respectively. Step by step:
\begin{enumerate}
\item Check if the spot for relocation is a base vertex $v^\mathcal{B}_i$ lying inside an adjacent face or on an edge connected to $v^{\Phi(\mathcal{M})}_A$.
\item Do an edge split or face split operation at $v^{\Phi(\mathcal{M})}_B$.
\item Collapse the old vertex $v^{\Phi(\mathcal{M})}_A$ into the new vertex $v^{\Phi(\mathcal{M})}_B$.
\end{enumerate}
In default cases as showcased above this works pretty well, however the embedded meta mesh structure permits self-edges so there needs to be a special look at triangles including self-edges.
\begin{figure}[ht]
\begin{centering}
\begin{subfigure}{.5\textwidth}
\def\svgwidth{0.9\linewidth}
{\centering
\input{img/RelocateCircleEdgeSplit.pdf_tex}\par
}
\vspace{-1pt}
\caption[width=.9\linewidth]{Vertex relocation from $v^{\Phi(\mathcal{M})}_A$ to $v^{\Phi(\mathcal{M})}_B$ on an edge of $E^{\Phi(\mathcal{M})}$.}
\label{fig:reloccircleedgesplit}
\end{subfigure}
\begin{subfigure}{.5\textwidth}
\def\svgwidth{0.9\linewidth}
{\centering
\input{img/RelocateCircleFaceSplit.pdf_tex}\par
}
\vspace{-1pt}
\caption[width=.9\linewidth]{Vertex relocation from $v^{\Phi(\mathcal{M})}_A$ to $v^{\Phi(\mathcal{M})}_B$ on a face of $F^{\Phi(\mathcal{M})}$.}
\label{fig:reloccirclefacesplit}
\end{subfigure}
\end{centering}
\caption{Vertex relocation as a concatenation of a split and a collapse in a triangle with a self-edge}
\label{fig:reloccirclewrapper}
\end{figure}
Figure \ref{fig:reloccirclewrapper} displays how a relocation would also work as a concatenation of a split and a collapse in a face with a self-edge, making it capable to relocate vertices in any possible face of our embedding.
However in some practical applications, it can be quite desirable to perform vertex relocations on a lower level rather than via concatenation. For instance when remeshing, performing low level relocations can help declutter patches of $\mathcal{B}$ by temporarily emptying them; something which relocation via concatenation doesn't do. However a low-level relocation is not applicable to all patches, so we keep both relocation by concatenation for stability, and low-level relocation for speed on simple patches.
Our low-level relocation works by removing all edges of a patch, inserting the new vertex, then retracing all edges \textit{in the correct order}. The order is particularly important in order to avoid edges blocking sectors or being traced along the wrong sides of handles. For relocation of a vertex $v^{\Phi(\mathcal{M})}_A$, the order goes as follows:
\begin{enumerate}
\item \textbf{Boundary edges}: These are traced first since they have a clearly defined embedding and should not be blocked by any other edges.
\item \textbf{Group representatives}: For each distinct group of the patch surrounding $v^{\Phi(\mathcal{M})}_A$, trace one edge connecting to each group. This builds a minimal spanning tree.
\item \textbf{Non-duplicate non-self edges}: Trace all edges that are not self-edges, but if there is more than one edge connecting two vertices $v^{\Phi(\mathcal{M})}_B$ and $v^{\Phi(\mathcal{M})}_C$, only trace one of them - otherwise the connectivity of some self-edges may be blocked.
\item \textbf{Self-edges}
\item \textbf{All remaining edges}
\end{enumerate}
These rules are somewhat complicated, but they are necessary for allowing the tracing of edges in a patch without interference, as long as the genus of that patch is 1 or lower. A collapse operation \textit{could} also be implemented in this way, but since we would still need the current edge bending collapse implementation for patches of higher genus, we choose not to do so.
\begin{figure}[ht]
\def\svgwidth{\textwidth}
{\centering
\input{img/RetracingDeadlock.pdf_tex}\par
}
\vspace{-1pt}
\caption{Collapsing Vertex B into Vertex A (a). Intermediate state (b). Correct retracing result (c). Wrong retracing result (d).}
\label{fig:retracingdeadlock}
\end{figure}
There are configurations which cannot be resolved by low-level retracing, from genus 2 and up. Figure \ref{fig:retracingdeadlock} shows such an example where two self-edges interlock in such a way that tracing them both would be impossible no matter in which order. In that case we need to fall back to concatenated relocation.
\section{Meta Mesh Updates}
\label{sec:MetaMeshUpdates}
In Section \ref{sec:Operations} we defined a series of \textit{atomic operations} which comprise a minimum set of operations necessary to change the topology of $\mathcal{M}$, without breaking out of the polymesh setting or splitting a mesh into more components. These operations are \textit{halfedge collapse}, \textit{edge rotation}, \textit{edge split} and \textit{face split}. Performing any of these operations will change the topology of $\mathcal{M}$, as well as the embedding of the edges $E^{\Phi(\mathcal{M})}$. This section explores how to implement these \textit{atomic operations} while maintaining and respecting our mesh embedding.
\input{ch/Operations/MetaMeshUpdates/EdgeRotation.tex}
\input{ch/Operations/MetaMeshUpdates/HalfedgeCollapse.tex}
\input{ch/Operations/MetaMeshUpdates/EdgeSplit.tex}
......
\subsection{Edge Rotation}
\label{subsec:EdgeRotation}
\begin{figure}[ht]
\def\svgwidth{\textwidth}
{\centering
\input{img/RotateHeader.pdf_tex}\par
}
\vspace{-1pt}
\caption{Edge rotation in a non-quad patch.}
\label{fig:RotateHeader}
\end{figure}
The simplest of the atomic mesh operations is edge rotation. Edge rotation moves an edge \textit{counter-clockwise} inside its surrounding patch. Naively, this could be implemented by deleting the edge and then inserting a new edge at the desired position. In practice, however, it is more efficient to instead change a few \textit{next\_halfedge\_handle} pointers, since this preserves our data structure and all other pointers from and to the edge, as well as the edge itself.
Most commonly, an edge rotation of an edge $e^{\mathcal{M}}_x$ is equivalent to an edge flip; this is the case if both adjacent faces are triangles. In every other case, rotation happens by reconnecting both halfedges $h^{\mathcal{M}}_{x1}$ and $h^{\mathcal{M}}_{x2}$ of $e^{\mathcal{M}}_x$ to the respective vertices of their successors $h^{\mathcal{M}}_{x1\_next}$ and $h^{\mathcal{M}}_{x2\_next}$. Figure \ref{fig:RotateHeader} shows how rotation on non-quad patches works.
\begin{wrapfigure}[19]{r}{0.5\textwidth}
\vspace{-15pt}
\def\svgwidth{0.5\textwidth}
{\centering
\input{img/RotateComplex.pdf_tex}\par
}
\vspace{-5pt}
\caption{(a-b): Rotation resulting in curve. (c-d): Rotation resulting in \textit{self-edge}.}
\label{fig:RotateComplex}
\end{wrapfigure}
In traditional surface meshes, there are certain restrictions when it comes to rotating edges. For instance, an edge cannot be rotated if the rotation would cause it to overlap with another edge, or if it would cause the edge to connect a vertex with itself. This is necessary mainly due to the fact that edges are straight lines. In an embedded setting, however, edges are very rarely straight and can in fact curve in different ways. This opens up the opportunity to perform almost\footnote{The exception are boundary edges, since flipping a boundary edge would break the boundary and also leave no space to trace the flipped edge in the first place. This could however be a limitation of our implementation; theoretically flipping boundary edges would be possible if adjustments of the underlying boundary are allowed.} any edge rotation on $\mathcal{M}$.
Figure \ref{fig:RotateComplex}.(a-b) shows an example of an edge rotation that would not be permissible in a traditional mesh. But by allowing curved edges, any rotation can be performed as long as it is possible to trace a curve in the patch surrounding it. Similarly, Figure \ref{fig:RotateComplex}.(c-d) starts from a mesh configuration with some curved edges since we can legally reach those. In configurations like this one, it is possible to rotate an edge such that after the rotation it connects a vertex with itself. This works, since there will always be at least one vertex inside the new \textit{self-edge}. When implementing a structure that allows \textit{self-edges}, it becomes important not to trace the \textit{self-edge} before at least one edge inside is traced first. Otherwise, the \textit{self-edge} will be traced as the smallest possible loop from the vertex to itself, and also block the way when attempting to trace any elements that should be inside the loop.
\subsection{Edge Split}
\label{subsec:EdgeSplit}
\begin{figure}[ht]
\def\svgwidth{\textwidth}
{\centering
\input{img/EdgeSplit1.pdf_tex}\par
}
\vspace{-1pt}
\caption{A split operation performed to add vertex $v^{\Phi(\mathcal{M})}_C$ on edge $e^{\Phi(\mathcal{M})}_{AB}$.}
\label{fig:edgesplit1}
\end{figure}
Another of the atomic mesh operations is the edge split. Figure \ref{fig:edgesplit1} shows how a split operation is performed on edge $e^{\Phi(\mathcal{M})}_{AB}$. The left part shows the neighborhood of edge $e^{\Phi(\mathcal{M})}_{AB}$, the middle part shows a new vertex $v^{\Phi(\mathcal{M})}_C$ being embedded into $e^{\Phi(\mathcal{M})}_{AB}$, and on the right part new edges are added to connect $v^{\Phi(\mathcal{M})}_C$ with the vertices incident to its faces.
In the embedded Meta Mesh structure normal splits can be performed in the same way as for normal meshes. Since the neighborhoods around the new vertex $v^{\Phi(\mathcal{M})}_C$ are simple, the order the new edges of $v^{\Phi(\mathcal{M})}_C$ are traced in is irrelevant too. However, there are special types of edges which do not occur in normal meshes which should be looked at.
The special types of edges that can occur in the embedded Meta Mesh structure but not in normal meshes are \textit{self-edges}, and edges with an incident vertex of valence 1\footnote{In a trimesh, vertices of valence 1 only occur with an adjacent \textit{self-edge}.}. When splitting a \textit{self-edge} there is nothing special to consider, since the two faces incident to the new vertex $v^{\Phi(\mathcal{M})}_C$ were simple before the split, and remain simple after it. After a split, a \textit{self-edge} naturally won't be a \textit{self-edge} anymore either. However edges with an incident vertex of valence 1 are a special case in that the two faces, next to the new vertex $v^{\Phi(\mathcal{M})}_C$, that need to be triangulated are the same.
\begin{figure}[ht]
\def\svgwidth{\textwidth}
{\centering
\input{img/EdgeSplit2.pdf_tex}\par
}
\vspace{-1pt}
\caption{A split being performed on an edge $e^{\Phi(\mathcal{M})}_{AB}$ with an incident vertex of valence 1, however there are two possible results (c-d).}
\label{fig:edgesplit2}
\end{figure}
Figure \ref{fig:edgesplit2} visualizes the explained example; on the left there is the neighborhood of edge $e^{\Phi(\mathcal{M})}_{AB}$ which is to be split; with $v^{\Phi(\mathcal{M})}_{B}$ being a vertex of valence 1. In (b), a vertex $v^{\Phi(\mathcal{M})}_C$ is inserted into $e^{\Phi(\mathcal{M})}_{AB}$, and here both faces incident to $v^{\Phi(\mathcal{M})}_C$ are identical. The resulting problem is that depending on which side of $v^{\Phi(\mathcal{M})}_C$ the triangulation of the face is started from there are different results (c-d). Since normal split operations are well-defined and will always lead to the same result, this should also be the case for all splits on $\Phi(\mathcal{M})$.
\begin{figure}[ht]
\def\svgwidth{\textwidth}
{\centering
\input{img/EdgeSplit3.pdf_tex}\par
}
\vspace{-1pt}
\caption{Comparison of the two possible triangulations (a-b) corresponding to Figure \ref{fig:edgesplit2}, and a third, rotated view (c).}
\label{fig:edgesplit3}
\end{figure}
Fortunately this is the case here too. On a closer look it can be seen that the two possible triangulations around $v^{\Phi(\mathcal{M})}_C$ are actually topologically identical. Figure \ref{fig:edgesplit3}.(a-b) illustrates it by color-coding the edges that are identical for those two triangulations. Figure \ref{fig:edgesplit3}.(c) is another topologically equivalent mesh which can be reached from either of the two previous meshes by rotating $v^{\Phi(\mathcal{M})}_C$ around its self-edge clockwise or counter-clockwise respectively.
With the special cases considered it is now clear that splits can be performed on all edges $e^{\Phi(\mathcal{M})}_i\in E^{\Phi(\mathcal{M})}$, consistent with the way splits work on regular meshes.
\ No newline at end of file
\subsection{Face Split}
\label{subsec:FaceSplit}
The last \textit{atomic operation} we define is the face split. It is very similar to the edge split discussed in the previous Section \ref{subsec:EdgeSplit}, except that the new split vertex is inserted into a face $f^{\Phi(\mathcal{M})}_i$ instead of an edge $e^{\Phi(\mathcal{M})}_i$. Figure \ref{fig:SplitFace1} shows how a face split is performed. Note that the embedding $\Phi(\mathcal{M})$ is discrete, so new vertices can only be inserted on positions of vertices in $V^\mathcal{B}$.
\begin{figure}[ht]
\begin{centering}
\begin{subfigure}{.5\textwidth}
\def\svgwidth{0.9\linewidth}
{\centering
\input{img/SplitFace1.pdf_tex}\par
}
\caption[width=.9\linewidth]{A split operation performed to add vertex $v^{\Phi(\mathcal{M})}_B$ on a face in $F^{\Phi(\mathcal{M})}$.}
\label{fig:SplitFace1}
\end{subfigure}
\begin{subfigure}{.5\textwidth}
\def\svgwidth{0.9\linewidth}
{\centering
\input{img/SplitFace2.pdf_tex}\par
}
\caption[width=.9\linewidth]{A split operation performed to add vertex $v^{\Phi(\mathcal{M})}_B$ on a face in $F^{\Phi(\mathcal{M})}$, where the boundary of the face contains a self-edge.}
\label{fig:SplitFace2}
\end{subfigure}
\end{centering}
\caption{Face splits on a normal face and a face including a self-edge.}
\label{fig:SplitFace}
\end{figure}
However, a naive implementation could also run into problems. When splitting a face on a regular mesh, it suffices to add an edge for each vertex on the patch, and the split is complete - as is done in Figure \ref{fig:SplitFace1}. But in cases where a faces boundary contains self-edges, the vertex of each self-edge has to be connected an additional time. Figure \ref{fig:SplitFace2} illustrates this; here the left vertex gets connected to the new vertex $v^{\Phi(\mathcal{M})}_B$ twice; connecting it only once would not result in a triangulation.
Blocking off of edges, or mistracing edges into the wrong sector cannot happen even with multiple edges connecting into the same vertex, as long as the trace operation considers entry sectors; more on that in Section \ref{subsec:RestrictedPathTracing}.
\ No newline at end of file
\subsection{Halfedge Collapse}
\label{subsec:HalfedgeCollapse}
\begin{figure}[ht]
\def\svgwidth{\textwidth}
{\centering
\input{img/RetracingDefault.pdf_tex}\par
}
\vspace{-1pt}
\caption{Collapsing embedded halfedge $h^{\Phi(\mathcal{M})}_{AB}$ from $v^{\Phi(\mathcal{M})}_A$ to $v^{\Phi(\mathcal{M})}_B$ (a). Intermediate state (b). Result after retracing (c).}
\label{fig:CollapseRetracingDefault}
\end{figure}
The second atomic operation is the halfedge collapse. A naive way of implementing a halfedge collapse of $h^{\mathcal{M}}_{AB}$ from $v^{\mathcal{M}}_A$ to $v^{\mathcal{M}}_B$ goes as follows:
\begin{enumerate}
\item Remove all embedded edges $e^{\Phi(\mathcal{M})}_i$ connected to $v^{\Phi(\mathcal{M})}_A$ from the embedding $\Phi(\mathcal{M})$, then remove $v^{\Phi(\mathcal{M})}_A$.
\item Perform the collapse operation on the meta mesh $\mathcal{M}$\footnote{This is a standard collapse implementation since $\mathcal{M}$ is a standard mesh}.
\item Trace all embedded edges $e^{\Phi(\mathcal{M})}_i$ whose underlying edges $e^{mathcal{M}}_i$ were \textbf{not} deleted in the previous step into $\Phi(\mathcal{M})$.
\end{enumerate}
Note that operations on the embedding have to be performed twice, both on $\mathcal{M}$ as well as $\Phi(\mathcal{M})$, in order to maintain the embedding. Figure \ref{fig:CollapseRetracingDefault} illustrates a halfedge collapse of $h^{\Phi(\mathcal{M})}_{AB}$ with an intermediate state, after all embedded edges connected to $v^{\Phi(\mathcal{M})}_A$ have been removed.
It is important to take a closer look at this intermediate state, otherwise problems can arise when retracing the halfedges post-collapse with new connectivity. In the example shown in Figure \ref{fig:CollapseRetracingDefault}, the intermediate patch is simple, and the deleted vertex $v^{\mathcal{M}}_A$ has no self-edges. Most patches are like that, but this is not always the case.
\begin{figure}[ht]
\def\svgwidth{\textwidth}
{\centering
\input{img/RetracingLoop.pdf_tex}\par
}
\vspace{-1pt}
\caption{Collapsing Vertex $v^{\Phi(\mathcal{M})}_A$ into Vertex $v^{\Phi(\mathcal{M})}_B$ (a). Intermediate state (b). Correct retracing result (c). Wrong retracing result (d).}
\label{fig:CollapseRetracingLoop}
\end{figure}
Figure \ref{fig:CollapseRetracingLoop} is such an example where a naive collapse implementation can lead to incorrect results. Notice that the patch around $v^{\mathcal{M}}_A$ is not simple, as it has two borders. $v^{\mathcal{M}}_A$ also has a self-edge. Naively retracing the edges of $v^{\mathcal{M}}_A$ after collapsing can lead to a situation as seen in Figure \ref{fig:CollapseRetracingLoop}.(d), where the self-edge gets traced first (as a minimal loop), and subsequent edges get \textit{stuck}\footnote{See the red arrows in Figure \ref{fig:CollapseRetracingLoop}.(d); they represent the Trace happening from vertex $v^{\mathcal{M}}_B$ and the vertex in the top left. As the self-edge created a new boundary, this trace operation fails.} as the self-edge blocks their tracing sector. In this case, this happened because the self-edge was originally going around the handle, but didn't after being retraced.
\begin{figure}[ht]
\def\svgwidth{\textwidth}
{\centering
\input{img/RetracingGroups.pdf_tex}\par
}
\vspace{-1pt}
\caption{Collapsing $v^{\Phi(\mathcal{M})}_A$ into Vertex $v^{\Phi(\mathcal{M})}_B$ (a). Intermediate state (b). Correct retracing result (c). Wrong retracing result (d).}
\label{fig:CollapseRetracingGroups}
\end{figure}
This type of problem can also occur with isolated groups of vertices. Any group of vertices enclosed by edges of another group can become unreachable, if the edges of the enclosing group are traced before the contents of the internal group. Figure \ref{fig:CollapseRetracingGroups} illustrates a halfedge collapse originating from a vertex $v^{\mathcal{M}}_A$ which has a self-edge enclosing a vertex of valence 1. Figure \ref{fig:CollapseRetracingGroups}.(d) illustrates what happens when the enclosing edges of the yellow vertex are traced first; the yellow vertex becomes unreachable.
The group problem can be solved by first building a minimal spanning tree for the groups of the sector. In Figure \ref{fig:CollapseRetracingGroups} for example there are three groups, color coded red, teal and yellow. Since $v^{\mathcal{M}}_B$ is color coded red, connecting it with one vertex of the teal and yellow groups builds that minimal spanning tree and avoids groups blocking each others traces. The spanning tree approach is similar to that used by Praun et al. \cite{praun2001consistent} during triangulation, and avoids most blocking problems.
\begin{figure}[ht]
\def\svgwidth{\textwidth}
{\centering
\input{img/EdgeBending.pdf_tex}\par
}
\vspace{-1pt}
\caption{\textit{Edge bending}: An iterative, locally stable way of collapsing a halfedge on $\Phi(\mathcal{M})$.}
\label{fig:EdgeBending}
\end{figure}
However, this is not enough to account for the existence of self-edges. In order to facilitate proper tracing of sectors, we propose an alternative way of performing collapses. Instead of removing all edges connected to $v^{\mathcal{M}}_A$, and then tracing new edges connected to $v^{\mathcal{M}}_B$ in an empty patch, we approach the collapse iteratively. We call this approach \textit{edge bending}; Figure \ref{fig:EdgeBending} illustrates this procedure in detail.
\begin{enumerate}
\item \textbf{(a)}: The initial patch is the same as can be seen in \ref{fig:CollapseRetracingDefault}, the halfedge $h^{\Phi(\mathcal{M})}_{AB}$ from $v^{\Phi(\mathcal{M})}_A$ to $v^{\Phi(\mathcal{M})}_B$ is to be collapsed.
\item \textbf{(b-f)}: We iteratively \textit{bend} each halfedge $h^{\Phi(\mathcal{M})}_{xA}$ ending in $v^{\Phi(\mathcal{M})}_A$, so that it ends in $v^{\Phi(\mathcal{M})}_B$ instead\footnote{Note that the first bent edge results in a loop if the first face is a triangle, and can be deleted immediately in that case, thus Figure \ref{fig:EdgeBending}.(b) starts by bending the \textit{second} edge.}. Each halfedge is immediately retraced after being bent; this ensures that each halfedge is traced inside a simple patch with no unconnected groups that could be cut off.
\item \textbf{(g)}: After $v^{\Phi(\mathcal{M})}_A$ has become a vertex of valence 1, it, as well as the halfedge $h^{\Phi(\mathcal{M})}_{AB}$, is removed from $\mathcal{M}$ and $\Phi(\mathcal{M})$. Loops are also removed in this step, such as the duplicate connection between $v^{\Phi(\mathcal{M})}_B$ and the connection to the vertex left of it after removing $h^{\Phi(\mathcal{M})}_{AB}$.
\item \textbf{(h)}: Lastly, the edges inside the patch are retraced in reverse order. This does not affect the topology and as such is not strictly necessary, but, as can be seen in Figure \ref{fig:EdgeBending}.(g), skipping this step leads to swirls and long edges which can become problems later on.
\end{enumerate}
The stability of this approach is guaranteed because every retrace operation on an edge $e^{\Phi(\mathcal{M})}_{i}\in E^{\Phi(\mathcal{M})}$ is performed in a patch $p^{\Phi(\mathcal{M})}_i$ consisting of two joined faces $f^{\Phi(\mathcal{M})}_1, f^{\Phi(\mathcal{M})}_2\in F^{\Phi(\mathcal{M})}$. These patches $p^{\Phi(\mathcal{M})}_i$ are always simple, because:
\begin{itemize}
\item \textbf{$f^{\Phi(\mathcal{M})}_1$ and $f^{\Phi(\mathcal{M})}_2$ are simple}: Every face in our implementation of the embedding has to be simple, this is an important restriction to make basic mesh operations work.
\item \textbf{$p^{\Phi(\mathcal{M})}_i$ results from joining $f^{\Phi(\mathcal{M})}_1$ and $f^{\Phi(\mathcal{M})}_2$ by removing one share edge $e^{\Phi(\mathcal{M})}_{f1|f2}$}: Joining two simple surfaces along a single shared boundary results in a simple surface.
\end{itemize}
\textbf{Proof}: Since $f^{\Phi(\mathcal{M})}_1$ is simple, it has a closed boundary $b^{\Phi(\mathcal{M})}_1$ of edges
$b^{\Phi(\mathcal{M})}_1:=\{e^{\mathcal{B}}_{i^,1}, e^{\mathcal{B}}_{i,2}, \dots, e^{\mathcal{B}}_{i,n}, e^{\mathcal{B}}_{i,1}\}$
which form a cycle. Similar for $f^{\Phi(\mathcal{M})}_2$ with boundary
$b^{\Phi(\mathcal{M})}_2:=\{e^{\mathcal{B}}_{j,1}, e^{\mathcal{B}}_{j,2}, \dots, e^{\mathcal{B}}_{j,m}, e^{\mathcal{B}}_{j,1}\}$.
Given a shared edge
$e^{\Phi(\mathcal{M})}_{f1|f2}:=\{e^{\mathcal{B}}_{i,k}, e^{\mathcal{B}}_{i,k+1}, \dots, e^{\mathcal{B}}_{i,l}\}=\{e^{\mathcal{B}}_{j,k}, e^{\mathcal{B}}_{j,k+1}, \dots, e^{\mathcal{B}}_{j,l}\}$,
deleting $e^{\Phi(\mathcal{M})}_{f1|f2}$ results in a patch $p^{\Phi(\mathcal{M})}_i$ with boundary
$b^{\Phi(\mathcal{M})}_p:=\{e^{\mathcal{B}}_{i,l+1},e^{\mathcal{B}}_{i,l+2}, \dots, e^{\mathcal{B}}_{i,n}, e^{\mathcal{B}}_{i,1}, e^{\mathcal{B}}_{i,2}, \dots, e^{\mathcal{B}}_{i,k}, e^{\mathcal{B}}_{j,k}, e^{\mathcal{B}}_{j,k-1},\dots,$
$e^{\mathcal{B}}_{j,m}, e^{\mathcal{B}}_{j,m-1}, \dots, e^{\mathcal{B}}_{j,l+1}, e^{\mathcal{B}}_{i,l+1}\}$.
Thus, $p^{\Phi(\mathcal{M})}_i$ has a closed boundary and is also simple.
Edge bending also has the additional benefit of elegantly dealing with self-edges. The only self-edges we need to consider as potential dangers to our retracing are those connected to $v^{\Phi(\mathcal{M})}_A$, since every face is guaranteed to be simple. When the edge bending iteration first reaches a self-edge $e^{\Phi(\mathcal{M})}_{self}$ on $v^{\Phi(\mathcal{M})}_A$, one of its vertex handles is changed to $v^{\Phi(\mathcal{M})}_B$; thus it is momentarily no longer a self-edge. When the self edge $e^{\Phi(\mathcal{M})}_{self}$ is iterated over a second time, everything inside the loop of $e^{\Phi(\mathcal{M})}_{self}$ has already been retraced, so tracing the $e^{\Phi(\mathcal{M})}_{self}$ becomes safe too.
\begin{figure}[ht]
\def\svgwidth{\textwidth}
{\centering
\input{img/val1edgecollapse.pdf_tex}\par
}
\vspace{-1pt}
\caption{1. Initial state 2. $h^{\Phi(\mathcal{M})}_{AB}$ collapse 3. 1-Loop removal 4. 2-Loop removal}
\label{fig:val1edgecollapse}
\end{figure}
\begin{wrapfigure}[10]{r}{0.50\textwidth}
\vspace{-1pt}
\def\svgwidth{0.4\textwidth}
{\centering
\input{img/Triangles.pdf_tex}\par
}
\vspace{-1pt}
\caption{A minimal trimesh in two different configurations}
\label{fig:triangles}
\end{wrapfigure}
Another special case to consider is the collapse of an edge with an incident vertex of valence 1. In principle, collapses like those are quite simple, since the edge and incident valence 1 vertex can simply be removed from the mesh, and no other topology has to change. However, those collapses introduce loops into the mesh which need to be removed. Figure \ref{fig:val1edgecollapse} shows a collapse of such an edge step by step.
\begin{wrapfigure}[9]{r}{0.50\textwidth}
\vspace{-50pt}
\def\svgwidth{0.4\textwidth}
{\centering
\input{img/1vertextorus.pdf_tex}\par
}
\vspace{-1pt}
\caption{A torus collapsed down to one vertex.}
\label{fig:1vertextorus}
\end{wrapfigure}
And lastly, collapses cannot be performed if the resulting mesh would stop being a polymesh after the collapse - e.g. have no faces left. This makes it necessary to determine whether a mesh component is minimal, and to forbid collapses in such a case. For example on surface meshes of disc topology there are exactly two minimal meshes as presented in Figure \ref{fig:triangles}. As a general rule, a face $f^\mathcal{M}_1$ is part of a minimal mesh component if and only if for each halfedge of its surrounding patch $h^\mathcal{M}_i\in p^\mathcal{M}_{f1}$ the opposite halfedge $h^\mathcal{M}_{i\_o}$ is adjacent to the same face $f^\mathcal{M}_2$.
With everything considered, our implementation of edge collapses is rebust enough to perform any legal collapse and is even able to collapse complicated meshes down to minimal meshes such as Figure \ref{fig:1vertextorus} which shows a meta mesh on a torus collapsed down to one vertex.
File added
%% Creator: Inkscape inkscape 0.92.4, www.inkscape.org
%% PDF/EPS/PS + LaTeX output extension by Johan Engelen, 2010
%% Accompanies image file '1vertextorus.pdf' (pdf, eps, ps)
%%
%% To include the image in your LaTeX document, write
%% \input{<filename>.pdf_tex}
%% instead of
%% \includegraphics{<filename>.pdf}
%% To scale the image, write
%% \def\svgwidth{<desired width>}
%% \input{<filename>.pdf_tex}
%% instead of
%% \includegraphics[width=<desired width>]{<filename>.pdf}
%%
%% Images with a different path to the parent latex file can
%% be accessed with the `import' package (which may need to be
%% installed) using
%% \usepackage{import}
%% in the preamble, and then including the image with
%% \import{<path to file>}{<filename>.pdf_tex}
%% Alternatively, one can specify
%% \graphicspath{{<path to file>/}}
%%
%% For more information, please see info/svg-inkscape on CTAN:
%% http://tug.ctan.org/tex-archive/info/svg-inkscape
%%
\begingroup%
\makeatletter%
\providecommand\color[2][]{%
\errmessage{(Inkscape) Color is used for the text in Inkscape, but the package 'color.sty' is not loaded}%
\renewcommand\color[2][]{}%
}%
\providecommand\transparent[1]{%
\errmessage{(Inkscape) Transparency is used (non-zero) for the text in Inkscape, but the package 'transparent.sty' is not loaded}%
\renewcommand\transparent[1]{}%
}%
\providecommand\rotatebox[2]{#2}%
\newcommand*\fsize{\dimexpr\f@size pt\relax}%
\newcommand*\lineheight[1]{\fontsize{\fsize}{#1\fsize}\selectfont}%
\ifx\svgwidth\undefined%
\setlength{\unitlength}{507.00001153bp}%
\ifx\svgscale\undefined%
\relax%
\else%
\setlength{\unitlength}{\unitlength * \real{\svgscale}}%
\fi%
\else%
\setlength{\unitlength}{\svgwidth}%
\fi%
\global\let\svgwidth\undefined%
\global\let\svgscale\undefined%
\makeatother%
\begin{picture}(1,0.78550291)%
\lineheight{1}%
\setlength\tabcolsep{0pt}%
\put(0,0){\includegraphics[width=\unitlength,page=1]{img/1vertextorus.pdf}}%
\end{picture}%
\endgroup%
File added
%% Creator: Inkscape inkscape 0.92.4, www.inkscape.org
%% PDF/EPS/PS + LaTeX output extension by Johan Engelen, 2010
%% Accompanies image file 'CatEdges.pdf' (pdf, eps, ps)
%%
%% To include the image in your LaTeX document, write
%% \input{<filename>.pdf_tex}
%% instead of
%% \includegraphics{<filename>.pdf}
%% To scale the image, write
%% \def\svgwidth{<desired width>}
%% \input{<filename>.pdf_tex}
%% instead of
%% \includegraphics[width=<desired width>]{<filename>.pdf}
%%
%% Images with a different path to the parent latex file can
%% be accessed with the `import' package (which may need to be
%% installed) using
%% \usepackage{import}
%% in the preamble, and then including the image with
%% \import{<path to file>}{<filename>.pdf_tex}
%% Alternatively, one can specify
%% \graphicspath{{<path to file>/}}
%%
%% For more information, please see info/svg-inkscape on CTAN:
%% http://tug.ctan.org/tex-archive/info/svg-inkscape
%%
\begingroup%
\makeatletter%
\providecommand\color[2][]{%
\errmessage{(Inkscape) Color is used for the text in Inkscape, but the package 'color.sty' is not loaded}%
\renewcommand\color[2][]{}%
}%
\providecommand\transparent[1]{%
\errmessage{(Inkscape) Transparency is used (non-zero) for the text in Inkscape, but the package 'transparent.sty' is not loaded}%
\renewcommand\transparent[1]{}%
}%
\providecommand\rotatebox[2]{#2}%
\newcommand*\fsize{\dimexpr\f@size pt\relax}%
\newcommand*\lineheight[1]{\fontsize{\fsize}{#1\fsize}\selectfont}%
\ifx\svgwidth\undefined%
\setlength{\unitlength}{611.9999827bp}%
\ifx\svgscale\undefined%
\relax%
\else%
\setlength{\unitlength}{\unitlength * \real{\svgscale}}%
\fi%
\else%
\setlength{\unitlength}{\svgwidth}%
\fi%
\global\let\svgwidth\undefined%
\global\let\svgscale\undefined%
\makeatother%
\begin{picture}(1,1.04044123)%
\lineheight{1}%
\setlength\tabcolsep{0pt}%
\put(0,0){\includegraphics[width=\unitlength,page=1]{img/CatEdges.pdf}}%
\put(0.82360435,0.99988619){\color[rgb]{0,0,0}\makebox(0,0)[lt]{\lineheight{1.25}\smash{\begin{tabular}[t]{l}A\end{tabular}}}}%
\put(0.3282399,0.11281557){\color[rgb]{0,0,0}\makebox(0,0)[lt]{\lineheight{1.25}\smash{\begin{tabular}[t]{l}B\end{tabular}}}}%
\put(0,0){\includegraphics[width=\unitlength,page=2]{img/CatEdges.pdf}}%
\end{picture}%
\endgroup%
File added
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment