menclose.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2018-2022 The MathJax Consortium
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * @fileoverview Implements the SVGmenclose wrapper for the MmlMenclose object
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {SVGWrapper, SVGConstructor} from '../Wrapper.js';
  23. import {CommonMencloseMixin} from '../../common/Wrappers/menclose.js';
  24. import {SVGmsqrt} from './msqrt.js';
  25. import * as Notation from '../Notation.js';
  26. import {MmlMenclose} from '../../../core/MmlTree/MmlNodes/menclose.js';
  27. import {OptionList} from '../../../util/Options.js';
  28. /*****************************************************************/
  29. /**
  30. * The SVGmenclose wrapper for the MmlMenclose object
  31. *
  32. * @template N The HTMLElement node class
  33. * @template T The Text node class
  34. * @template D The Document class
  35. */
  36. // @ts-ignore
  37. export class SVGmenclose<N, T, D> extends CommonMencloseMixin<
  38. SVGWrapper<any, any, any>,
  39. SVGmsqrt<any, any, any>,
  40. any,
  41. SVGConstructor<any, any, any>
  42. >(SVGWrapper) {
  43. /**
  44. * The menclose wrapper
  45. */
  46. public static kind = MmlMenclose.prototype.kind;
  47. /**
  48. * The definitions of the various notations
  49. */
  50. public static notations: Notation.DefList<SVGmenclose<any, any, any>, any> = new Map([
  51. Notation.Border('top'),
  52. Notation.Border('right'),
  53. Notation.Border('bottom'),
  54. Notation.Border('left'),
  55. Notation.Border2('actuarial', 'top', 'right'),
  56. Notation.Border2('madruwb', 'bottom', 'right'),
  57. Notation.DiagonalStrike('up'),
  58. Notation.DiagonalStrike('down'),
  59. ['horizontalstrike', {
  60. renderer: Notation.RenderLine('horizontal', 'Y'),
  61. bbox: (node) => [0, node.padding, 0, node.padding]
  62. }],
  63. ['verticalstrike', {
  64. renderer: Notation.RenderLine('vertical', 'X'),
  65. bbox: (node) => [node.padding, 0, node.padding, 0]
  66. }],
  67. ['box', {
  68. renderer: (node, _child) => {
  69. const {w, h, d} = node.getBBox();
  70. node.adaptor.append(node.element, node.box(w, h, d));
  71. },
  72. bbox: Notation.fullBBox,
  73. border: Notation.fullBorder,
  74. remove: 'left right top bottom'
  75. }],
  76. ['roundedbox', {
  77. renderer: (node, _child) => {
  78. const {w, h, d} = node.getBBox();
  79. const r = node.thickness + node.padding;
  80. node.adaptor.append(node.element, node.box(w, h, d, r));
  81. },
  82. bbox: Notation.fullBBox
  83. }],
  84. ['circle', {
  85. renderer: (node, _child) => {
  86. const {w, h, d} = node.getBBox();
  87. node.adaptor.append(node.element, node.ellipse(w, h, d));
  88. },
  89. bbox: Notation.fullBBox
  90. }],
  91. ['phasorangle', {
  92. //
  93. // Use a bottom border and an upward strike properly angled
  94. //
  95. renderer: (node, _child) => {
  96. const {w, h, d} = node.getBBox();
  97. const a = node.getArgMod(1.75 * node.padding, h + d)[0];
  98. const t = node.thickness / 2;
  99. const HD = h + d;
  100. const cos = Math.cos(a);
  101. node.adaptor.append(
  102. node.element,
  103. node.path('mitre', 'M', w, t - d, 'L', t + cos * t, t - d, 'L' , cos * HD + t, HD - d - t)
  104. );
  105. },
  106. bbox: (node) => {
  107. const p = node.padding / 2;
  108. const t = node.thickness;
  109. return [2 * p, p, p + t, 3 * p + t];
  110. },
  111. border: (node) => [0, 0, node.thickness, 0],
  112. remove: 'bottom'
  113. }],
  114. Notation.Arrow('up'),
  115. Notation.Arrow('down'),
  116. Notation.Arrow('left'),
  117. Notation.Arrow('right'),
  118. Notation.Arrow('updown'),
  119. Notation.Arrow('leftright'),
  120. Notation.DiagonalArrow('updiagonal'), // backward compatibility
  121. Notation.DiagonalArrow('northeast'),
  122. Notation.DiagonalArrow('southeast'),
  123. Notation.DiagonalArrow('northwest'),
  124. Notation.DiagonalArrow('southwest'),
  125. Notation.DiagonalArrow('northeastsouthwest'),
  126. Notation.DiagonalArrow('northwestsoutheast'),
  127. ['longdiv', {
  128. //
  129. // Use a line along the top followed by a half ellipse at the left
  130. //
  131. renderer: (node, _child) => {
  132. const {w, h, d} = node.getBBox();
  133. const t = node.thickness / 2;
  134. const p = node.padding;
  135. node.adaptor.append(
  136. node.element,
  137. node.path('round',
  138. 'M', t, t - d,
  139. 'a', p - t / 2, (h + d) / 2 - 4 * t, 0, '0,1', 0, h + d - 2 * t,
  140. 'L', w - t, h - t
  141. )
  142. );
  143. },
  144. bbox: (node) => {
  145. const p = node.padding;
  146. const t = node.thickness;
  147. return [p + t, p, p, 2 * p + t / 2];
  148. }
  149. }],
  150. ['radical', {
  151. //
  152. // Use the msqrt rendering, but remove the extra space due to the radical
  153. // (it is added in at the end, so other notations overlap the root)
  154. //
  155. renderer: (node, child) => {
  156. node.msqrt.toSVG(child);
  157. const left = node.sqrtTRBL()[3];
  158. node.place(-left, 0, child);
  159. },
  160. //
  161. // Create the needed msqrt wrapper
  162. //
  163. init: (node) => {
  164. node.msqrt = node.createMsqrt(node.childNodes[0]);
  165. },
  166. //
  167. // Add back in the padding for the square root
  168. //
  169. bbox: (node) => node.sqrtTRBL(),
  170. //
  171. // This notation replaces the child
  172. //
  173. renderChild: true
  174. }]
  175. ] as Notation.DefPair<SVGmenclose<any, any, any>, any>[]);
  176. /********************************************************/
  177. /**
  178. * @override
  179. */
  180. public toSVG(parent: N) {
  181. const svg = this.standardSVGnode(parent);
  182. //
  183. // Create a box at the correct position and add the children
  184. //
  185. const left = this.getBBoxExtenders()[3];
  186. const def: OptionList = {};
  187. if (left > 0) {
  188. def.transform = 'translate(' + this.fixed(left) + ', 0)';
  189. }
  190. const block = this.adaptor.append(svg, this.svg('g', def));
  191. if (this.renderChild) {
  192. this.renderChild(this, block);
  193. } else {
  194. this.childNodes[0].toSVG(block);
  195. }
  196. //
  197. // Render all the notations for this menclose element
  198. //
  199. for (const name of Object.keys(this.notations)) {
  200. const notation = this.notations[name];
  201. !notation.renderChild && notation.renderer(this, svg);
  202. }
  203. }
  204. /********************************************************/
  205. /**
  206. * Create an arrow using SVG elements
  207. *
  208. * @param {number} W The length of the arrow
  209. * @param {number} a The angle for the arrow
  210. * @param {boolean} double True if this is a double-headed arrow
  211. * @param {string} offset 'X' for vertical arrow, 'Y' for horizontal
  212. * @param {number} dist Distance to translate in the offset direction
  213. * @return {N} The newly created arrow
  214. */
  215. public arrow(W: number, a: number, double: boolean, offset: string = '', dist: number = 0): N {
  216. const {w, h, d} = this.getBBox();
  217. const dw = (W - w) / 2;
  218. const m = (h - d) / 2;
  219. const t = this.thickness;
  220. const t2 = t / 2;
  221. const [x, y, dx] = [t * this.arrowhead.x, t * this.arrowhead.y, t * this.arrowhead.dx];
  222. const arrow =
  223. (double ?
  224. this.fill(
  225. 'M', w + dw, m, // point of arrow
  226. 'l', -(x + dx), y, 'l', dx, t2 - y, // upper right head
  227. 'L', x - dw, m + t2, // upper side of shaft
  228. 'l', dx, y - t2, 'l', -(x + dx), - y, // left point
  229. 'l', x + dx, -y, 'l', -dx, y - t2, // lower left head
  230. 'L', w + dw - x, m - t2, // lower side of shaft
  231. 'l', -dx, t2 - y, 'Z' // lower head
  232. ) :
  233. this.fill(
  234. 'M', w + dw, m, // point of arrow
  235. 'l', -(x + dx), y, 'l', dx, t2 - y, // upper head
  236. 'L', -dw, m + t2, 'l', 0, -t, // upper side of shaft
  237. 'L', w + dw - x, m - t2, // lower side of shaft
  238. 'l', -dx, t2 - y, 'Z' // lower head
  239. ));
  240. const transform = [];
  241. if (dist) {
  242. transform.push(offset === 'X' ? `translate(${this.fixed(-dist)} 0)` : `translate(0 ${this.fixed(dist)})`);
  243. }
  244. if (a) {
  245. const A = this.jax.fixed(-a * 180 / Math.PI);
  246. transform.push(`rotate(${A} ${this.fixed(w / 2)} ${this.fixed(m)})`);
  247. }
  248. if (transform.length) {
  249. this.adaptor.setAttribute(arrow, 'transform', transform.join(' '));
  250. }
  251. return arrow as N;
  252. }
  253. /********************************************************/
  254. /**
  255. * Create a line element
  256. *
  257. * @param {number[]} pq The coordinates of the endpoints, [x1, y1, x2, y2]
  258. * @return {N} The newly created line element
  259. */
  260. public line(pq: [number, number, number, number]): N {
  261. const [x1, y1, x2, y2] = pq;
  262. return this.svg('line', {
  263. x1: this.fixed(x1), y1: this.fixed(y1),
  264. x2: this.fixed(x2), y2: this.fixed(y2),
  265. 'stroke-width': this.fixed(this.thickness)
  266. });
  267. }
  268. /**
  269. * Create a rectangle element
  270. *
  271. * @param {number} w The width of the rectangle
  272. * @param {number} h The height of the rectangle
  273. * @param {number} d The depth of the rectangle
  274. * @param {number=} r The corner radius for a rounded rectangle
  275. * @return {N} The newly created line element
  276. */
  277. public box(w: number, h: number, d: number, r: number = 0): N {
  278. const t = this.thickness;
  279. const def: OptionList = {
  280. x: this.fixed(t / 2), y: this.fixed(t / 2 - d),
  281. width: this.fixed(w - t), height: this.fixed(h + d - t),
  282. fill: 'none', 'stroke-width': this.fixed(t)
  283. };
  284. if (r) {
  285. def.rx = this.fixed(r);
  286. }
  287. return this.svg('rect', def);
  288. }
  289. /**
  290. * Create an ellipse element
  291. *
  292. * @param {number} w The width of the ellipse
  293. * @param {number} h The height of the ellipse
  294. * @param {number} d The depth of the ellipse
  295. * @return {N} The newly created ellipse node
  296. */
  297. public ellipse(w: number, h: number, d: number): N {
  298. const t = this.thickness;
  299. return this.svg('ellipse', {
  300. rx: this.fixed((w - t) / 2), ry: this.fixed((h + d - t) / 2),
  301. cx: this.fixed(w / 2), cy: this.fixed((h - d) / 2),
  302. 'fill': 'none', 'stroke-width': this.fixed(t)
  303. });
  304. }
  305. /**
  306. * Create a path element from the commands that specify it
  307. *
  308. * @param {string} join The join style for the path
  309. * @param {(string|number)[]} P The list of commands and coordinates for the path
  310. * @return {N} The newly created path
  311. */
  312. public path(join: string, ...P: (string | number)[]): N {
  313. return this.svg('path', {
  314. d: P.map(x => (typeof x === 'string' ? x : this.fixed(x))).join(' '),
  315. style: {'stroke-width': this.fixed(this.thickness)},
  316. 'stroke-linecap': 'round', 'stroke-linejoin': join,
  317. fill: 'none'
  318. });
  319. }
  320. /**
  321. * Create a filled path element from the commands the specify it
  322. * (same as path above, but no thickness adjustments)
  323. *
  324. * @param {(string|number)[]} P The list of commands and coordinates for the path
  325. * @return {N} The newly created path
  326. */
  327. public fill(...P: (string | number)[]): N {
  328. return this.svg('path', {
  329. d: P.map(x => (typeof x === 'string' ? x : this.fixed(x))).join(' ')
  330. });
  331. }
  332. }