forum icon indicating copy to clipboard operation
forum copied to clipboard

`\mathchoice` 和 LaTeX3 的变量展开问题

Open xkwxdyy opened this issue 3 years ago • 12 comments

问题背景

我在用 tikz 给 exam-zh 绘制中国化(主要是初高中)的部分数学符号,比如 \subset, \cap 等。在 @syvshc 的提醒下,我发现需要考虑到符号在不同模式(比如正文、角标)中的大小问题,然后在 tex.se 上查到一个回答 Define a math symbol relative to font size。里面提到了使用 \mathchoice 来处理。

MWE

\documentclass{article}
\usepackage{tikz}

\begin{document}

\makeatletter
\ExplSyntaxOn
% 直线长度
\dim_new:N \l__examzh_symbols_cap_line_length_dim
\dim_set:Nn \l__examzh_symbols_cap_line_length_dim { 0.56em }
% 半径大小
\dim_new:N \l__examzh_symbols_cap_radius_dim
\dim_set:Nn \l__examzh_symbols_cap_radius_dim { 0.28em }


\cs_new:Npn \__examzh_symbols_cap:
  {
    \begin{tikzpicture}[line~cap=round, line~width = 0.6pt,baseline = {([yshift = 1.2pt]current~bounding~box.south)}]
      \draw (\l__examzh_symbols_cap_radius_dim,0) arc (0 \c_colon_str 180 \c_colon_str \l__examzh_symbols_cap_radius_dim);
      \draw 
        (\l__examzh_symbols_cap_radius_dim,0) --++ (0,-\l__examzh_symbols_cap_line_length_dim)
        (-\l__examzh_symbols_cap_radius_dim,0) --++ (0,-\l__examzh_symbols_cap_line_length_dim);
    \end{tikzpicture}
  }
\cs_set_eq:NN \__examzh_symbols_old_cap: \cap
\RenewDocumentCommand { \cap } { s }
  {
    \IfBooleanTF {#1}
      { \__examzh_symbols_old_cap: }
      {
        \mathrel 
          { 
            \__examzh_symbols_symbol_four_size:n 
              { \__examzh_symbols_cap: } 
          }
      }
  }
\cs_new:Npn \__examzh_symbols_symbol_four_size:n #1
  {
    \mathchoice
      {
        \hbox:n 
          {
            \fontsize{\tf@size}{\tf@size}\selectfont #1
          }
      }
      {
        \hbox:n 
          {
            \fontsize{\tf@size}{\tf@size}\selectfont #1
          }
      }
      {
        \hbox:n
          {
            \fontsize{\sf@size}{\sf@size}\selectfont #1
          }
      }
      {
        \hbox:n
          {
            \fontsize{\ssf@size}{\ssf@size}\selectfont #1 
          }
      }
  }
\ExplSyntaxOff
\makeatother

$A\cap B_{A\cap B}$

{\large$A\cap B$\par}

{\Large$A\cap B$\par}

{\LARGE$A\cap B$\par}

{\small$A\cap B$\par}

{\footnotesize$A\cap B$\par}


$A\cap* B_{A\cap* B}$

{\large$A\cap* B$\par}

{\Large$A\cap* B$\par}

{\LARGE$A\cap* B$\par}

{\small$A\cap* B$\par}

{\footnotesize$A\cap* B$\par}

\end{document}

结果发现在不同模式下重定义的 \cap 大小是一样的: image

分析

因为之前在 LaTeX 工作室 b站视频,项子越讲 LaTeX3 的 LaTeX3 教程三:宏展开 时候讲到了一个 lazy evaluation 的想法,我猜想这里也可能是这样的,于是我将其中的参数变量去掉,

\documentclass{article}
\usepackage{tikz}

\begin{document}

\makeatletter
\ExplSyntaxOn
\cs_new:Npn \__examzh_symbols_cap:
{
  \begin{tikzpicture}[line~cap=round, line~width = 0.6pt,baseline = {([yshift = 1.2pt]current~bounding~box.south)}]
    \draw (0.28em,0) arc (0 \c_colon_str 180 \c_colon_str 0.28em);
    \draw 
      (0.28em,0) --++ (0,-0.56em)
      (-0.28em,0) --++ (0,-0.56em);
  \end{tikzpicture}
}
\cs_set_eq:NN \__examzh_symbols_old_cap: \cap
\RenewDocumentCommand { \cap } { s }
{
  \IfBooleanTF {#1}
    { \__examzh_symbols_old_cap: }
    {
      \mathrel 
        { 
          \__examzh_symbols_symbol_four_size:n 
            { \__examzh_symbols_cap: } 
        }
    }
}
\cs_new:Npn \__examzh_symbols_symbol_four_size:n #1
{
  \mathchoice
    {
      \hbox:n 
        {
          \fontsize{\tf@size}{\tf@size}\selectfont #1
        }
    }
    {
      \hbox:n 
        {
          \fontsize{\tf@size}{\tf@size}\selectfont #1
        }
    }
    {
      \hbox:n
        {
          \fontsize{\sf@size}{\sf@size}\selectfont #1
        }
    }
    {
      \hbox:n
        {
          \fontsize{\ssf@size}{\ssf@size}\selectfont #1 
        }
    }
}
\ExplSyntaxOff
\makeatother

$A\cap B_{A\cap B}$

{\large$A\cap B$\par}

{\Large$A\cap B$\par}

{\LARGE$A\cap B$\par}

{\small$A\cap B$\par}

{\footnotesize$A\cap B$\par}

\end{document}

发现不同模式的大小有所区别,是理想的效果: image

所以我大概猜想问题出于展开的顺序

  1. 使用 \cap 时,先展开为 \mathchoice... 然后根据环境判断
  2. 选择其中一个之后,继续展开为 ... \__examzh_symbols_cap:
  3. 然后 \__examzh_symbols_cap: 展开为 tikzpicture 环境

接下来我就不知道怎么分析了,但感觉貌似变量的 em 的相对大小是在 dim_set 这个函数作用「时刻」相对,所以也就是在正文的大小。

请教一下如何解决这个问题?

xkwxdyy avatar Jul 07 '22 11:07 xkwxdyy

\mathchoice 无关。tex 的 dimension 只能存储绝对长度,所有相对单位(em, ex, mu)都会使用当前字号转换为绝对单位。内部储存时用的 sp,用 \the<dimen> 时总是使用 pt。在下面的例子里,三处 \showthe 往 log 和 stdout 输出的内容相同,都是 10.0002pt

\newlength{\mylen} \mylen=1em

     \showthe\mylen
\tiny\showthe\mylen
\Huge\showthe\mylen

于是 preamble 里的 \dim_set:Nn \l__examzh_symbols_cap_line_length_dim { 0.56em } 就把 l__examzh_symbols_cap_line_length_dim 唯一确定了。办法就是,在具体命令内部才给这些 dimension 赋(使用相对单位的)值。

muzimuzhi avatar Jul 07 '22 12:07 muzimuzhi

\mathchoice 无关。tex 的 dimension 只能存储绝对长度,所有相对单位(em, ex, mu)都会使用当前字号转换为绝对单位。内部储存时用的 sp,用 \the<dimen> 时总是使用 pt。在下面的例子里,三处 \showthe 往 log 和 stdout 输出的内容相同,都是 10.0002pt

\newlength{\mylen} \mylen=1em

     \showthe\mylen
\tiny\showthe\mylen
\Huge\showthe\mylen

于是 preamble 里的 \dim_set:Nn \l__examzh_symbols_cap_line_length_dim { 0.56em } 就把 l__examzh_symbols_cap_line_length_dim 唯一确定了。办法就是,在具体命令内部才给这些 dimension 赋(使用相对单位的)值。

我把设置放在了命令里确实可以了,但我发现 \scriptstyle\scriptscriptstyle 都是 \textstyle 的大小:

\documentclass{article}
\usepackage{tikz}

\begin{document}

\makeatletter
\ExplSyntaxOn
\keys_define:nn { exam-zh / symbols }
  {
    cap-line-length .dim_set:N = \l__examzh_symbols_cap_line_length_dim,
    cup-line-length .dim_set:N = \l__examzh_symbols_cup_line_length_dim,
    cap-radius .dim_set:N = \l__examzh_symbols_cap_radius_dim,
    cup-radius .dim_set:N = \l__examzh_symbols_cup_radius_dim,
  }
\cs_new:Npn \__examzh_symbols_cap:
  {
    \begin{tikzpicture}[line~cap=round, line~width = 0.6pt,baseline = {([yshift = 1.2pt]current~bounding~box.south)}]
      \draw (\l__examzh_symbols_cap_radius_dim,0) arc (0 \c_colon_str 180 \c_colon_str \l__examzh_symbols_cap_radius_dim);
      \draw 
        (\l__examzh_symbols_cap_radius_dim,0) --++ (0,-\l__examzh_symbols_cap_line_length_dim)
        (-\l__examzh_symbols_cap_radius_dim,0) --++ (0,-\l__examzh_symbols_cap_line_length_dim);
    \end{tikzpicture}
  }
\cs_set_eq:NN \__examzh_symbols_old_cap: \cap
\RenewDocumentCommand { \cap } { s }
  {
    \group_begin:
      \keys_set:nn { exam-zh / symbols }
        {
          cap-line-length = 0.56em,
          cap-radius      = 0.28em
        }
      \IfBooleanTF {#1}
        { \use:c { __examzh_symbols_old_cap : } }
        {
          \mathrel 
            { 
              \__examzh_symbols_symbol_four_size:n 
                { \__examzh_symbols_cap: } 
            }
        }
    \group_end:
  }
\cs_new:Npn \__examzh_symbols_symbol_four_size:n #1
  {
    \mathchoice
      {
        \hbox:n 
          {
            \fontsize{\tf@size}{\tf@size}\selectfont #1
          }
      }
      {
        \hbox:n 
          {
            \fontsize{\tf@size}{\tf@size}\selectfont #1
          }
      }
      {
        \hbox:n
          {
            \fontsize{\sf@size}{\sf@size}\selectfont #1
          }
      }
      {
        \hbox:n
          {
            \fontsize{\ssf@size}{\ssf@size}\selectfont #1 
          }
      }
  }
\ExplSyntaxOff
\makeatother

$A\cap B_{A\cap B}$

$A\cap B_{A{\scriptstyle \cap} B}$

$A{\scriptscriptstyle \cap} B$

$A\cap B_{A{\scriptscriptstyle \cap} B}$


{\large$A\cap B$\par}

{\Large$A\cap B$\par}

{\LARGE$A\cap B$\par}

{\small$A\cap B$\par}

{\footnotesize$A\cap B$\par}

\end{document}
image

xkwxdyy avatar Jul 07 '22 13:07 xkwxdyy

我把设置放在了命令里确实可以了,但我发现 \scriptstyle\scriptscriptstyle 都是 \textstyle 的大小:

\keys_set:nn 还是用得太早了,导致 \mathchoice 里的 \fontsize{\tf@size}{\tf@size}\selectfont 等完全没起作用。可以用 latex2e kernel 提供的 \mathpalette 或者 amsmath 里的 \text,部分代替你的 \__examzh_symbols_symbol_four_size:n

我不了解你的完整需求,只从提供的例子看,有一点「为了 latex3 而 late3」的感觉。latex3 只是基于 primitives 的一套宏,它不提供任何新的、用 primitives、用 latex2e 表达不了的特性。

注意,你要实现的,其实是参数化地设计字符,类似 https://www.zhihu.com/question/352249034/answer/870883390 。要想出来的字符在各种尺寸都好看,会需要越来越多的参数,在 tex 里的实现会越来越复杂、运行越来越耗时。其实也可以设计个新字体,只提供需要替换的符号。

下面的例子展示了,「好看」的 \cap (包括它的前后空白)不能只靠「画一个固定的图案、让它随字号放缩」实现。得有随字号变化的参数。

\documentclass{article}
\usepackage{amsmath}
\usepackage{tikz}

\makeatletter
\ExplSyntaxOn
\keys_define:nn { exam-zh / symbols }
  {
    cap-line-width  .tl_set:N = \c__examzh_symbols_cap_line_width_tl,
    cap-height      .tl_set:N = \c__examzh_symbols_cap_height_tl,
    cap-radius      .tl_set:N = \c__examzh_symbols_cap_radius_tl,
  }

\keys_set:nn { exam-zh / symbols }
  {
    cap-line-width  = 0.06em,
    cap-height      = 0.56em,
    cap-radius      = 0.28em,
  }

\ExplSyntaxOff
% \__examzh_symbols_cap:nnn {<line-width>} {<height>} {<radius>}
\csname cs_new:cn\endcsname {__examzh_symbols_cap:nnn}
  {%
    \begin{tikzpicture}[
      line cap=round,
      line width=#1,
      baseline={([yshift=#1*2]current bounding box.south)}
    ]
      \draw (#3,0) -- +(0,#2) arc (0:180:#3) -- +(0,-#2);
    \end{tikzpicture}%
  }
\ExplSyntaxOn
\cs_generate_variant:Nn \__examzh_symbols_cap:nnn {VVV}

\cs_set_eq:NN \__examzh_symbols_old_cap: \cap
\RenewDocumentCommand { \cap } { s }
  {
    \IfBooleanTF {#1}
      { \__examzh_symbols_old_cap: }
      {
        \__examzh_symbols_cap_wrap:n
          {
            \__examzh_symbols_cap:VVV
              \c__examzh_symbols_cap_line_width_tl
              \c__examzh_symbols_cap_height_tl
              \c__examzh_symbols_cap_radius_tl
          }
      }
  }

\cs_new:Nn \__examzh_symbols_cap_wrap:n
  {
    \mathrel { \mathpalette {} { \text {#1} } }
  }
\ExplSyntaxOff
\makeatother

\begin{document}

\def\test#1{
  {#1\par
  \noindent\texttt{\detokenize{#1}}\par
  \texttt{\string\cap*}: $A \cap* B_{A \cap* B_{A \cap* B}}$\par
  \texttt{\string\cap\ }: $A \cap B_{A \cap B_{A \cap B}}$\par
  }
}

\test\scriptsize
\test\normalsize
\test\Large
\end{document}

image

muzimuzhi avatar Jul 07 '22 17:07 muzimuzhi

非常感谢!我马上去测试一下

xkwxdyy avatar Jul 08 '22 02:07 xkwxdyy

注意,你要实现的,其实是参数化地设计字符,类似 https://www.zhihu.com/question/352249034/answer/870883390 。要想出来的字符在各种尺寸都好看,会需要越来越多的参数,在 tex 里的实现会越来越复杂、运行越来越耗时。其实也可以设计个新字体,只提供需要替换的符号。

好的,后续完善了之后会学习这方面的知识~

xkwxdyy avatar Jul 08 '22 02:07 xkwxdyy

\documentclass{article}
\usepackage{amsmath}
\usepackage{tikz}

\makeatletter
% \__examzh_symbols_cap_symbol:nnn {<line-width>} {<height>} {<radius>}
\csname cs_new_protected:cn\endcsname {__examzh_symbols_cap_symbol:nnn}
  {%
    \begin{tikzpicture}[
      line cap=round,
      line width=#1,
      baseline={([yshift=#1*2]current bounding box.south)}
    ]
      \draw (#3,0) -- +(0,#2) arc (0:180:#3) -- +(0,-#2);
    \end{tikzpicture}%
  }

\ExplSyntaxOn
\NewCommandCopy \__examzh_symbols_old_cap: \cap
\RenewDocumentCommand { \cap } { s }
  {
    \IfBooleanTF {#1}
      { \__examzh_symbols_old_cap: }
      { \__examzh_symbols_cap: }
  }

\cs_new_protected:Nn \__examzh_symbols_cap:
  {
    \mathrel 
      {
        \mathchoice
          { \__examzh_symbols_cap_wrapper:nnnn {.06em} {.56em} {.28em} {0mu} }
          { \__examzh_symbols_cap_wrapper:nnnn {.06em} {.56em} {.28em} {0mu} }
          { \__examzh_symbols_cap_wrapper:nnnn {.05em} {.50em} {.25em} {2mu} }
          { \__examzh_symbols_cap_wrapper:nnnn {.04em} {.46em} {.23em} {2mu} }
      }
  }

% \__examzh_symbols_cap_wrapper:nnnn 
%   {<line-width>} {<height>} {<radius>} {<sep>}
\cs_new_protected:Nn \__examzh_symbols_cap_wrapper:nnnn
  {
     % mind the gap: 
     % https://github.com/latex3/latex3/issues/575#issuecomment-571342176
     \mkern #4 % is mu really necessary here??
     \text { \__examzh_symbols_cap_buffer:nnn {#1} {#2} {#3} }
     \mkern #4
  }

\cs_new:Nn \__examzh_symbols_cap_buffer:nnn
  {
    \exp_args:Nc \__examzh_symbols_cap_buffer:Nnnn
      { __examzh_cap_ \f@size _#1_#2_#3 } {#1} {#2} {#3}
  }

\cs_new_protected:Nn \__examzh_symbols_cap_buffer:Nnnn
  {
    \cs_if_exist:NTF #1
      { \box_use:N #1 }
      {
        \box_new:N #1
        \hbox_gset:Nn #1
          { $ \__examzh_symbols_cap_symbol:nnn {#2} {#3} {#4} $ }
        \box_use:N #1
      }
  }
\ExplSyntaxOff
\makeatother

\begin{document}

\def\test#1{
  {#1\par
  \noindent\texttt{\detokenize{#1}}\par
  \texttt{\string\cap*}:
    $\displaystyle A \cap* B \textstyle A \cap* B_{A \cap* B_{A \cap* B}}$\par
  \texttt{\string\cap\ }:
    $\displaystyle A \cap B \textstyle A \cap B_{A \cap B_{A \cap B}}$\par
  }
}

\foreach \i in {
  \tiny, \scriptsize, \footnotesize, \small,
  \normalsize,
  \large, \Large, \LARGE, \huge, \Huge
} {\expandafter\test\i}
\end{document}

image

觉得……还是应该去造字体。

muzimuzhi avatar Jul 15 '22 12:07 muzimuzhi

\csname cs_new_protected:cn\endcsname {__examzh_symbols_cap_symbol:nnn}

请问一下这里为什么要这样用,而不是

\ExplSyntaxOn
\cs_new_protected:cn \__examzh_symbols_cap_symbol:nnn #1#2#3
  {...}
\ExplSyntaxOff

xkwxdyy avatar Jul 15 '22 13:07 xkwxdyy

觉得……还是应该去造字体。

这个超出了我目前的能力了(所以也是为什么一直还没处理这个符号的原因(我的锅)),暑假会去学习相关知识的。

感谢您的回复 @muzimuzhi

xkwxdyy avatar Jul 15 '22 13:07 xkwxdyy

\csname cs_new_protected:cn\endcsname {__examzh_symbols_cap_symbol:nnn}

请问一下这里为什么要这样用,而不是

\ExplSyntaxOn
\cs_new_protected:cn \__examzh_symbols_cap_symbol:nnn #1#2#3
  {...}
\ExplSyntaxOff

应该是因为下面就可以不用把空格和冒号特殊处理了吧?

xkwxdyy avatar Jul 15 '22 13:07 xkwxdyy

是。\__examzh_symbols_cap:nnn 的定义是一个 tikzpicture,让空格和冒号恢复默认 catcodes,写起来和读起来都更方便。

写成 \protected\@namedef{__examzh_symbols_cap:nnn} #1#2#3 {...} 也是可以的。

你举的 \cs_new_protected:cn \__examzh_symbols_cap_symbol:nnn #1#2#3 {...} 不大对。如果后面的 \__examzh_symbols_cap_symbol:nnn #1#2#3 {...} 不变,前面应该用 \cs_new_protected:Npn

muzimuzhi avatar Jul 15 '22 13:07 muzimuzhi

是。\__examzh_symbols_cap:nnn 的定义是一个 tikzpicture,让空格和冒号恢复默认 catcodes,更可读。

写成 \protected\@namedef{__examzh_symbols_cap:nnn} #1#2#3 {...} 也是可以的。

你举的 \cs_new_protected:cn \__examzh_symbols_cap_symbol:nnn #1#2#3 {...} 不大对。如果后面的 \__examzh_symbols_cap_symbol:nnn #1#2#3 {...} 不变,前面应该用 \cs_new_protected:Npn

感谢。是我疏忽了这个 p。平时都用 Npn , cpn,一下没看出问题。

xkwxdyy avatar Jul 15 '22 14:07 xkwxdyy

提个建议,不要用类似于“中国化”的表达方式。

AlphaZTX avatar Oct 12 '22 07:10 AlphaZTX