| 
									
										
										
										
											2021-12-28 19:22:57 +08:00
										 |  |  | import { getFontString, arrayToMap, isTestEnv } from "../utils"; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | import { | 
					
						
							|  |  |  |   ExcalidrawElement, | 
					
						
							| 
									
										
										
										
											2022-11-25 18:15:34 +08:00
										 |  |  |   ExcalidrawTextContainer, | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |   ExcalidrawTextElement, | 
					
						
							| 
									
										
										
										
											2022-01-05 20:28:03 +08:00
										 |  |  |   ExcalidrawTextElementWithContainer, | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  |   FontFamilyValues, | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |   FontString, | 
					
						
							|  |  |  |   NonDeletedExcalidrawElement, | 
					
						
							|  |  |  | } from "./types"; | 
					
						
							| 
									
										
										
										
											2020-03-15 12:48:51 +08:00
										 |  |  | import { mutateElement } from "./mutateElement"; | 
					
						
							| 
									
										
										
										
											2023-03-13 22:16:09 +08:00
										 |  |  | import { | 
					
						
							| 
									
										
										
										
											2023-08-02 18:04:21 +08:00
										 |  |  |   ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO, | 
					
						
							|  |  |  |   ARROW_LABEL_WIDTH_FRACTION, | 
					
						
							| 
									
										
										
										
											2023-03-13 22:16:09 +08:00
										 |  |  |   BOUND_TEXT_PADDING, | 
					
						
							|  |  |  |   DEFAULT_FONT_FAMILY, | 
					
						
							|  |  |  |   DEFAULT_FONT_SIZE, | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  |   FONT_FAMILY, | 
					
						
							| 
									
										
										
										
											2023-04-10 21:22:46 +08:00
										 |  |  |   isSafari, | 
					
						
							| 
									
										
										
										
											2023-03-13 22:16:09 +08:00
										 |  |  |   TEXT_ALIGN, | 
					
						
							|  |  |  |   VERTICAL_ALIGN, | 
					
						
							|  |  |  | } from "../constants"; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | import { MaybeTransformHandleType } from "./transformHandles"; | 
					
						
							|  |  |  | import Scene from "../scene/Scene"; | 
					
						
							| 
									
										
										
										
											2022-03-03 00:04:09 +08:00
										 |  |  | import { isTextElement } from "."; | 
					
						
							| 
									
										
										
										
											2023-03-03 20:10:42 +08:00
										 |  |  | import { isBoundToContainer, isArrowElement } from "./typeChecks"; | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  | import { LinearElementEditor } from "./linearElementEditor"; | 
					
						
							|  |  |  | import { AppState } from "../types"; | 
					
						
							| 
									
										
										
										
											2022-11-25 18:15:34 +08:00
										 |  |  | import { isTextBindableContainer } from "./typeChecks"; | 
					
						
							|  |  |  | import { getElementAbsoluteCoords } from "../element"; | 
					
						
							|  |  |  | import { getSelectedElements } from "../scene"; | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  | import { isHittingElementNotConsideringBoundingBox } from "./collision"; | 
					
						
							| 
									
										
										
										
											2022-12-23 14:27:48 +08:00
										 |  |  | import { | 
					
						
							|  |  |  |   resetOriginalContainerCache, | 
					
						
							|  |  |  |   updateOriginalContainerCache, | 
					
						
							|  |  |  | } from "./textWysiwyg"; | 
					
						
							| 
									
										
										
										
											2023-03-05 02:21:57 +08:00
										 |  |  | import { ExtractSetType } from "../utility-types"; | 
					
						
							| 
									
										
										
										
											2020-01-10 22:00:19 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-27 06:44:26 +08:00
										 |  |  | export const normalizeText = (text: string) => { | 
					
						
							|  |  |  |   return ( | 
					
						
							|  |  |  |     text | 
					
						
							|  |  |  |       // replace tabs with spaces so they render and measure correctly
 | 
					
						
							|  |  |  |       .replace(/\t/g, "        ") | 
					
						
							|  |  |  |       // normalize newlines
 | 
					
						
							|  |  |  |       .replace(/\r?\n|\r/g, "\n") | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  | export const splitIntoLines = (text: string) => { | 
					
						
							|  |  |  |   return normalizeText(text).split("\n"); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-23 19:32:35 +08:00
										 |  |  | export const redrawTextBoundingBox = ( | 
					
						
							| 
									
										
										
										
											2022-09-22 18:10:38 +08:00
										 |  |  |   textElement: ExcalidrawTextElement, | 
					
						
							| 
									
										
										
										
											2021-12-23 19:32:35 +08:00
										 |  |  |   container: ExcalidrawElement | null, | 
					
						
							|  |  |  | ) => { | 
					
						
							| 
									
										
										
										
											2022-09-19 18:00:37 +08:00
										 |  |  |   let maxWidth = undefined; | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   const boundTextUpdates = { | 
					
						
							|  |  |  |     x: textElement.x, | 
					
						
							|  |  |  |     y: textElement.y, | 
					
						
							|  |  |  |     text: textElement.text, | 
					
						
							|  |  |  |     width: textElement.width, | 
					
						
							|  |  |  |     height: textElement.height, | 
					
						
							| 
									
										
										
										
											2023-04-10 21:22:46 +08:00
										 |  |  |     baseline: textElement.baseline, | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   boundTextUpdates.text = textElement.text; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-04 20:32:16 +08:00
										 |  |  |   if (container) { | 
					
						
							| 
									
										
										
										
											2023-08-02 18:04:21 +08:00
										 |  |  |     maxWidth = getBoundTextMaxWidth(container, textElement); | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |     boundTextUpdates.text = wrapText( | 
					
						
							| 
									
										
										
										
											2022-09-22 18:10:38 +08:00
										 |  |  |       textElement.originalText, | 
					
						
							|  |  |  |       getFontString(textElement), | 
					
						
							| 
									
										
										
										
											2022-11-08 22:20:41 +08:00
										 |  |  |       maxWidth, | 
					
						
							| 
									
										
										
										
											2021-12-29 19:19:52 +08:00
										 |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   const metrics = measureText( | 
					
						
							|  |  |  |     boundTextUpdates.text, | 
					
						
							|  |  |  |     getFontString(textElement), | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  |     textElement.lineHeight, | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   boundTextUpdates.width = metrics.width; | 
					
						
							|  |  |  |   boundTextUpdates.height = metrics.height; | 
					
						
							| 
									
										
										
										
											2023-04-10 21:22:46 +08:00
										 |  |  |   boundTextUpdates.baseline = metrics.baseline; | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-29 19:19:52 +08:00
										 |  |  |   if (container) { | 
					
						
							| 
									
										
										
										
											2023-04-25 20:36:23 +08:00
										 |  |  |     const maxContainerHeight = getBoundTextMaxHeight( | 
					
						
							|  |  |  |       container, | 
					
						
							|  |  |  |       textElement as ExcalidrawTextElementWithContainer, | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2023-08-09 19:11:15 +08:00
										 |  |  |     const maxContainerWidth = getBoundTextMaxWidth(container); | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-03 20:11:34 +08:00
										 |  |  |     if (!isArrowElement(container) && metrics.height > maxContainerHeight) { | 
					
						
							| 
									
										
										
										
											2023-08-09 19:11:15 +08:00
										 |  |  |       const nextHeight = computeContainerDimensionForBoundText( | 
					
						
							| 
									
										
										
										
											2023-04-25 20:27:53 +08:00
										 |  |  |         metrics.height, | 
					
						
							|  |  |  |         container.type, | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |       mutateElement(container, { height: nextHeight }); | 
					
						
							|  |  |  |       updateOriginalContainerCache(container.id, nextHeight); | 
					
						
							| 
									
										
										
										
											2022-09-22 18:10:38 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-08-09 19:11:15 +08:00
										 |  |  |     if (metrics.width > maxContainerWidth) { | 
					
						
							|  |  |  |       const nextWidth = computeContainerDimensionForBoundText( | 
					
						
							|  |  |  |         metrics.width, | 
					
						
							|  |  |  |         container.type, | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |       mutateElement(container, { width: nextWidth }); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-04-25 20:27:53 +08:00
										 |  |  |     const updatedTextElement = { | 
					
						
							|  |  |  |       ...textElement, | 
					
						
							|  |  |  |       ...boundTextUpdates, | 
					
						
							|  |  |  |     } as ExcalidrawTextElementWithContainer; | 
					
						
							|  |  |  |     const { x, y } = computeBoundTextPosition(container, updatedTextElement); | 
					
						
							|  |  |  |     boundTextUpdates.x = x; | 
					
						
							|  |  |  |     boundTextUpdates.y = y; | 
					
						
							| 
									
										
										
										
											2021-12-29 19:19:52 +08:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   mutateElement(textElement, boundTextUpdates); | 
					
						
							| 
									
										
										
										
											2020-01-10 22:00:19 +08:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | export const bindTextToShapeAfterDuplication = ( | 
					
						
							|  |  |  |   sceneElements: ExcalidrawElement[], | 
					
						
							|  |  |  |   oldElements: ExcalidrawElement[], | 
					
						
							|  |  |  |   oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>, | 
					
						
							|  |  |  | ): void => { | 
					
						
							|  |  |  |   const sceneElementMap = arrayToMap(sceneElements) as Map< | 
					
						
							|  |  |  |     ExcalidrawElement["id"], | 
					
						
							|  |  |  |     ExcalidrawElement | 
					
						
							|  |  |  |   >; | 
					
						
							|  |  |  |   oldElements.forEach((element) => { | 
					
						
							|  |  |  |     const newElementId = oldIdToDuplicatedId.get(element.id) as string; | 
					
						
							|  |  |  |     const boundTextElementId = getBoundTextElementId(element); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (boundTextElementId) { | 
					
						
							| 
									
										
										
										
											2022-03-03 00:04:09 +08:00
										 |  |  |       const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId); | 
					
						
							|  |  |  |       if (newTextElementId) { | 
					
						
							|  |  |  |         const newContainer = sceneElementMap.get(newElementId); | 
					
						
							|  |  |  |         if (newContainer) { | 
					
						
							|  |  |  |           mutateElement(newContainer, { | 
					
						
							| 
									
										
										
										
											2023-02-02 16:23:39 +08:00
										 |  |  |             boundElements: (element.boundElements || []) | 
					
						
							|  |  |  |               .filter( | 
					
						
							|  |  |  |                 (boundElement) => | 
					
						
							|  |  |  |                   boundElement.id !== newTextElementId && | 
					
						
							|  |  |  |                   boundElement.id !== boundTextElementId, | 
					
						
							|  |  |  |               ) | 
					
						
							|  |  |  |               .concat({ | 
					
						
							|  |  |  |                 type: "text", | 
					
						
							|  |  |  |                 id: newTextElementId, | 
					
						
							|  |  |  |               }), | 
					
						
							| 
									
										
										
										
											2022-03-03 00:04:09 +08:00
										 |  |  |           }); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         const newTextElement = sceneElementMap.get(newTextElementId); | 
					
						
							|  |  |  |         if (newTextElement && isTextElement(newTextElement)) { | 
					
						
							|  |  |  |           mutateElement(newTextElement, { | 
					
						
							|  |  |  |             containerId: newContainer ? newElementId : null, | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const handleBindTextResize = ( | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |   container: NonDeletedExcalidrawElement, | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |   transformHandleType: MaybeTransformHandleType, | 
					
						
							| 
									
										
										
										
											2023-08-02 18:04:21 +08:00
										 |  |  |   shouldMaintainAspectRatio = false, | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | ) => { | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |   const boundTextElementId = getBoundTextElementId(container); | 
					
						
							|  |  |  |   if (!boundTextElementId) { | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2022-12-23 14:27:48 +08:00
										 |  |  |   resetOriginalContainerCache(container.id); | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |   let textElement = Scene.getScene(container)!.getElement( | 
					
						
							|  |  |  |     boundTextElementId, | 
					
						
							|  |  |  |   ) as ExcalidrawTextElement; | 
					
						
							|  |  |  |   if (textElement && textElement.text) { | 
					
						
							|  |  |  |     if (!container) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     textElement = Scene.getScene(container)!.getElement( | 
					
						
							| 
									
										
										
										
											2022-02-21 19:45:29 +08:00
										 |  |  |       boundTextElementId, | 
					
						
							|  |  |  |     ) as ExcalidrawTextElement; | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |     let text = textElement.text; | 
					
						
							|  |  |  |     let nextHeight = textElement.height; | 
					
						
							|  |  |  |     let nextWidth = textElement.width; | 
					
						
							| 
									
										
										
										
											2023-04-25 20:36:23 +08:00
										 |  |  |     const maxWidth = getBoundTextMaxWidth(container); | 
					
						
							|  |  |  |     const maxHeight = getBoundTextMaxHeight( | 
					
						
							|  |  |  |       container, | 
					
						
							|  |  |  |       textElement as ExcalidrawTextElementWithContainer, | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2023-08-02 18:04:21 +08:00
										 |  |  |     let containerHeight = container.height; | 
					
						
							| 
									
										
										
										
											2023-04-10 21:22:46 +08:00
										 |  |  |     let nextBaseLine = textElement.baseline; | 
					
						
							| 
									
										
										
										
											2023-08-02 18:04:21 +08:00
										 |  |  |     if ( | 
					
						
							|  |  |  |       shouldMaintainAspectRatio || | 
					
						
							|  |  |  |       (transformHandleType !== "n" && transformHandleType !== "s") | 
					
						
							|  |  |  |     ) { | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |       if (text) { | 
					
						
							|  |  |  |         text = wrapText( | 
					
						
							|  |  |  |           textElement.originalText, | 
					
						
							| 
									
										
										
										
											2022-02-21 19:45:29 +08:00
										 |  |  |           getFontString(textElement), | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |           maxWidth, | 
					
						
							| 
									
										
										
										
											2022-02-21 19:45:29 +08:00
										 |  |  |         ); | 
					
						
							| 
									
										
										
										
											2022-03-02 22:36:07 +08:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2023-04-10 21:22:46 +08:00
										 |  |  |       const metrics = measureText( | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  |         text, | 
					
						
							|  |  |  |         getFontString(textElement), | 
					
						
							|  |  |  |         textElement.lineHeight, | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2023-04-10 21:22:46 +08:00
										 |  |  |       nextHeight = metrics.height; | 
					
						
							|  |  |  |       nextWidth = metrics.width; | 
					
						
							|  |  |  |       nextBaseLine = metrics.baseline; | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |     } | 
					
						
							|  |  |  |     // increase height in case text element height exceeds
 | 
					
						
							|  |  |  |     if (nextHeight > maxHeight) { | 
					
						
							| 
									
										
										
										
											2023-03-03 20:10:42 +08:00
										 |  |  |       containerHeight = computeContainerDimensionForBoundText( | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |         nextHeight, | 
					
						
							| 
									
										
										
										
											2023-03-03 20:10:42 +08:00
										 |  |  |         container.type, | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |       ); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-02 18:04:21 +08:00
										 |  |  |       const diff = containerHeight - container.height; | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |       // fix the y coord when resizing from ne/nw/n
 | 
					
						
							|  |  |  |       const updatedY = | 
					
						
							|  |  |  |         !isArrowElement(container) && | 
					
						
							|  |  |  |         (transformHandleType === "ne" || | 
					
						
							|  |  |  |           transformHandleType === "nw" || | 
					
						
							|  |  |  |           transformHandleType === "n") | 
					
						
							|  |  |  |           ? container.y - diff | 
					
						
							|  |  |  |           : container.y; | 
					
						
							|  |  |  |       mutateElement(container, { | 
					
						
							|  |  |  |         height: containerHeight, | 
					
						
							| 
									
										
										
										
											2022-02-21 19:45:29 +08:00
										 |  |  |         y: updatedY, | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     mutateElement(textElement, { | 
					
						
							|  |  |  |       text, | 
					
						
							|  |  |  |       width: nextWidth, | 
					
						
							|  |  |  |       height: nextHeight, | 
					
						
							| 
									
										
										
										
											2023-04-10 21:22:46 +08:00
										 |  |  |       baseline: nextBaseLine, | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |     if (!isArrowElement(container)) { | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |       mutateElement( | 
					
						
							|  |  |  |         textElement, | 
					
						
							|  |  |  |         computeBoundTextPosition( | 
					
						
							|  |  |  |           container, | 
					
						
							|  |  |  |           textElement as ExcalidrawTextElementWithContainer, | 
					
						
							|  |  |  |         ), | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |       ); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-02-21 19:45:29 +08:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-13 14:15:58 +08:00
										 |  |  | export const computeBoundTextPosition = ( | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |   container: ExcalidrawElement, | 
					
						
							|  |  |  |   boundTextElement: ExcalidrawTextElementWithContainer, | 
					
						
							|  |  |  | ) => { | 
					
						
							| 
									
										
										
										
											2023-04-13 14:15:58 +08:00
										 |  |  |   if (isArrowElement(container)) { | 
					
						
							|  |  |  |     return LinearElementEditor.getBoundTextElementPosition( | 
					
						
							|  |  |  |       container, | 
					
						
							|  |  |  |       boundTextElement, | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   const containerCoords = getContainerCoords(container); | 
					
						
							| 
									
										
										
										
											2023-04-25 20:36:23 +08:00
										 |  |  |   const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement); | 
					
						
							|  |  |  |   const maxContainerWidth = getBoundTextMaxWidth(container); | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   let x; | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |   let y; | 
					
						
							|  |  |  |   if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) { | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |     y = containerCoords.y; | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |   } else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) { | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |     y = containerCoords.y + (maxContainerHeight - boundTextElement.height); | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |   } else { | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |     y = | 
					
						
							|  |  |  |       containerCoords.y + | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |       (maxContainerHeight / 2 - boundTextElement.height / 2); | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   } | 
					
						
							|  |  |  |   if (boundTextElement.textAlign === TEXT_ALIGN.LEFT) { | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |     x = containerCoords.x; | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   } else if (boundTextElement.textAlign === TEXT_ALIGN.RIGHT) { | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |     x = containerCoords.x + (maxContainerWidth - boundTextElement.width); | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   } else { | 
					
						
							|  |  |  |     x = | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |       containerCoords.x + (maxContainerWidth / 2 - boundTextElement.width / 2); | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   } | 
					
						
							|  |  |  |   return { x, y }; | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | // https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
 | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  | export const measureText = ( | 
					
						
							|  |  |  |   text: string, | 
					
						
							|  |  |  |   font: FontString, | 
					
						
							|  |  |  |   lineHeight: ExcalidrawTextElement["lineHeight"], | 
					
						
							|  |  |  | ) => { | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |   text = text | 
					
						
							|  |  |  |     .split("\n") | 
					
						
							|  |  |  |     // replace empty lines with single space because leading/trailing empty
 | 
					
						
							|  |  |  |     // lines would be stripped from computation
 | 
					
						
							|  |  |  |     .map((x) => x || " ") | 
					
						
							|  |  |  |     .join("\n"); | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  |   const fontSize = parseFloat(font); | 
					
						
							|  |  |  |   const height = getTextHeight(text, fontSize, lineHeight); | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  |   const width = getTextWidth(text, font); | 
					
						
							| 
									
										
										
										
											2023-04-10 21:22:46 +08:00
										 |  |  |   const baseline = measureBaseline(text, font, lineHeight); | 
					
						
							|  |  |  |   return { width, height, baseline }; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const measureBaseline = ( | 
					
						
							|  |  |  |   text: string, | 
					
						
							|  |  |  |   font: FontString, | 
					
						
							|  |  |  |   lineHeight: ExcalidrawTextElement["lineHeight"], | 
					
						
							|  |  |  |   wrapInContainer?: boolean, | 
					
						
							|  |  |  | ) => { | 
					
						
							|  |  |  |   const container = document.createElement("div"); | 
					
						
							|  |  |  |   container.style.position = "absolute"; | 
					
						
							|  |  |  |   container.style.whiteSpace = "pre"; | 
					
						
							|  |  |  |   container.style.font = font; | 
					
						
							|  |  |  |   container.style.minHeight = "1em"; | 
					
						
							|  |  |  |   if (wrapInContainer) { | 
					
						
							|  |  |  |     container.style.overflow = "hidden"; | 
					
						
							|  |  |  |     container.style.wordBreak = "break-word"; | 
					
						
							|  |  |  |     container.style.whiteSpace = "pre-wrap"; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   container.style.lineHeight = String(lineHeight); | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-10 21:22:46 +08:00
										 |  |  |   container.innerText = text; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Baseline is important for positioning text on canvas
 | 
					
						
							|  |  |  |   document.body.appendChild(container); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const span = document.createElement("span"); | 
					
						
							|  |  |  |   span.style.display = "inline-block"; | 
					
						
							|  |  |  |   span.style.overflow = "hidden"; | 
					
						
							|  |  |  |   span.style.width = "1px"; | 
					
						
							|  |  |  |   span.style.height = "1px"; | 
					
						
							|  |  |  |   container.appendChild(span); | 
					
						
							|  |  |  |   let baseline = span.offsetTop + span.offsetHeight; | 
					
						
							|  |  |  |   const height = container.offsetHeight; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (isSafari) { | 
					
						
							|  |  |  |     const canvasHeight = getTextHeight(text, parseFloat(font), lineHeight); | 
					
						
							|  |  |  |     const fontSize = parseFloat(font); | 
					
						
							|  |  |  |     // In Safari the font size gets rounded off when rendering hence calculating the safari height and shifting the baseline if it differs
 | 
					
						
							|  |  |  |     // from the actual canvas height
 | 
					
						
							|  |  |  |     const domHeight = getTextHeight(text, Math.round(fontSize), lineHeight); | 
					
						
							|  |  |  |     if (canvasHeight > height) { | 
					
						
							|  |  |  |       baseline += canvasHeight - domHeight; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (height > canvasHeight) { | 
					
						
							|  |  |  |       baseline -= domHeight - canvasHeight; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   document.body.removeChild(container); | 
					
						
							|  |  |  |   return baseline; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * To get unitless line-height (if unknown) we can calculate it by dividing | 
					
						
							|  |  |  |  * height-per-line by fontSize. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export const detectLineHeight = (textElement: ExcalidrawTextElement) => { | 
					
						
							|  |  |  |   const lineCount = splitIntoLines(textElement.text).length; | 
					
						
							|  |  |  |   return (textElement.height / | 
					
						
							|  |  |  |     lineCount / | 
					
						
							|  |  |  |     textElement.fontSize) as ExcalidrawTextElement["lineHeight"]; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * We calculate the line height from the font size and the unitless line height, | 
					
						
							|  |  |  |  * aligning with the W3C spec. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export const getLineHeightInPx = ( | 
					
						
							|  |  |  |   fontSize: ExcalidrawTextElement["fontSize"], | 
					
						
							|  |  |  |   lineHeight: ExcalidrawTextElement["lineHeight"], | 
					
						
							|  |  |  | ) => { | 
					
						
							|  |  |  |   return fontSize * lineHeight; | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2023-02-23 20:09:02 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  | // FIXME rename to getApproxMinContainerHeight
 | 
					
						
							|  |  |  | export const getApproxMinLineHeight = ( | 
					
						
							|  |  |  |   fontSize: ExcalidrawTextElement["fontSize"], | 
					
						
							|  |  |  |   lineHeight: ExcalidrawTextElement["lineHeight"], | 
					
						
							|  |  |  | ) => { | 
					
						
							|  |  |  |   return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | let canvas: HTMLCanvasElement | undefined; | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-24 00:15:49 +08:00
										 |  |  | const getLineWidth = (text: string, font: FontString) => { | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |   if (!canvas) { | 
					
						
							|  |  |  |     canvas = document.createElement("canvas"); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const canvas2dContext = canvas.getContext("2d")!; | 
					
						
							|  |  |  |   canvas2dContext.font = font; | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  |   const width = canvas2dContext.measureText(text).width; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-28 19:22:57 +08:00
										 |  |  |   // since in test env the canvas measureText algo
 | 
					
						
							|  |  |  |   // doesn't measure text and instead just returns number of
 | 
					
						
							|  |  |  |   // characters hence we assume that each letteris 10px
 | 
					
						
							|  |  |  |   if (isTestEnv()) { | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  |     return width * 10; | 
					
						
							| 
									
										
										
										
											2021-12-28 19:22:57 +08:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  |   return width; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-24 00:15:49 +08:00
										 |  |  | export const getTextWidth = (text: string, font: FontString) => { | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  |   const lines = splitIntoLines(text); | 
					
						
							| 
									
										
										
										
											2022-12-24 00:15:49 +08:00
										 |  |  |   let width = 0; | 
					
						
							|  |  |  |   lines.forEach((line) => { | 
					
						
							|  |  |  |     width = Math.max(width, getLineWidth(line, font)); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   return width; | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  | export const getTextHeight = ( | 
					
						
							|  |  |  |   text: string, | 
					
						
							|  |  |  |   fontSize: number, | 
					
						
							|  |  |  |   lineHeight: ExcalidrawTextElement["lineHeight"], | 
					
						
							|  |  |  | ) => { | 
					
						
							|  |  |  |   const lineCount = splitIntoLines(text).length; | 
					
						
							|  |  |  |   return getLineHeightInPx(fontSize, lineHeight) * lineCount; | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-20 13:40:46 +08:00
										 |  |  | export const parseTokens = (text: string) => { | 
					
						
							|  |  |  |   // Splitting words containing "-" as those are treated as separate words
 | 
					
						
							|  |  |  |   // by css wrapping algorithm eg non-profit => non-, profit
 | 
					
						
							|  |  |  |   const words = text.split("-"); | 
					
						
							|  |  |  |   if (words.length > 1) { | 
					
						
							|  |  |  |     // non-proft org => ['non-', 'profit org']
 | 
					
						
							|  |  |  |     words.forEach((word, index) => { | 
					
						
							|  |  |  |       if (index !== words.length - 1) { | 
					
						
							|  |  |  |         words[index] = word += "-"; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   // Joining the words with space and splitting them again with space to get the
 | 
					
						
							|  |  |  |   // final list of tokens
 | 
					
						
							|  |  |  |   // ['non-', 'profit org'] =>,'non- proft org' => ['non-','profit','org']
 | 
					
						
							|  |  |  |   return words.join(" ").split(" "); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-22 18:10:38 +08:00
										 |  |  | export const wrapText = (text: string, font: FontString, maxWidth: number) => { | 
					
						
							| 
									
										
										
										
											2023-03-20 20:20:09 +08:00
										 |  |  |   // if maxWidth is not finite or NaN which can happen in case of bugs in
 | 
					
						
							|  |  |  |   // computation, we need to make sure we don't continue as we'll end up
 | 
					
						
							|  |  |  |   // in an infinite loop
 | 
					
						
							|  |  |  |   if (!Number.isFinite(maxWidth) || maxWidth < 0) { | 
					
						
							|  |  |  |     return text; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |   const lines: Array<string> = []; | 
					
						
							|  |  |  |   const originalLines = text.split("\n"); | 
					
						
							| 
									
										
										
										
											2022-12-24 00:15:49 +08:00
										 |  |  |   const spaceWidth = getLineWidth(" ", font); | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   let currentLine = ""; | 
					
						
							|  |  |  |   let currentLineWidthTillNow = 0; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-21 15:02:43 +08:00
										 |  |  |   const push = (str: string) => { | 
					
						
							|  |  |  |     if (str.trim()) { | 
					
						
							|  |  |  |       lines.push(str); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   const resetParams = () => { | 
					
						
							|  |  |  |     currentLine = ""; | 
					
						
							|  |  |  |     currentLineWidthTillNow = 0; | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |   originalLines.forEach((originalLine) => { | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  |     const currentLineWidth = getTextWidth(originalLine, font); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-20 13:40:46 +08:00
										 |  |  |     // Push the line if its <= maxWidth
 | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  |     if (currentLineWidth <= maxWidth) { | 
					
						
							|  |  |  |       lines.push(originalLine); | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |       return; // continue
 | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-20 13:40:46 +08:00
										 |  |  |     const words = parseTokens(originalLine); | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  |     resetParams(); | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |     let index = 0; | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |     while (index < words.length) { | 
					
						
							|  |  |  |       const currentWordWidth = getLineWidth(words[index], font); | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  |       // This will only happen when single word takes entire width
 | 
					
						
							|  |  |  |       if (currentWordWidth === maxWidth) { | 
					
						
							|  |  |  |         push(words[index]); | 
					
						
							|  |  |  |         index++; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |       // Start breaking longer words exceeding max width
 | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  |       else if (currentWordWidth > maxWidth) { | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |         // push current line since the current word exceeds the max width
 | 
					
						
							|  |  |  |         // so will be appended in next line
 | 
					
						
							| 
									
										
										
										
											2023-04-20 13:40:46 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |         push(currentLine); | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         resetParams(); | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |         while (words[index].length > 0) { | 
					
						
							|  |  |  |           const currentChar = String.fromCodePoint( | 
					
						
							|  |  |  |             words[index].codePointAt(0)!, | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  |           const width = charWidth.calculate(currentChar, font); | 
					
						
							|  |  |  |           currentLineWidthTillNow += width; | 
					
						
							|  |  |  |           words[index] = words[index].slice(currentChar.length); | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |           if (currentLineWidthTillNow >= maxWidth) { | 
					
						
							| 
									
										
										
										
											2022-12-21 15:02:43 +08:00
										 |  |  |             push(currentLine); | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |             currentLine = currentChar; | 
					
						
							|  |  |  |             currentLineWidthTillNow = width; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |           } else { | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |             currentLine += currentChar; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |         } | 
					
						
							|  |  |  |         // push current line if appending space exceeds max width
 | 
					
						
							|  |  |  |         if (currentLineWidthTillNow + spaceWidth >= maxWidth) { | 
					
						
							|  |  |  |           push(currentLine); | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  |           resetParams(); | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |           // space needs to be appended before next word
 | 
					
						
							|  |  |  |           // as currentLine contains chars which couldn't be appended
 | 
					
						
							| 
									
										
										
										
											2023-04-20 13:40:46 +08:00
										 |  |  |           // to previous line unless the line ends with hyphen to sync
 | 
					
						
							|  |  |  |           // with css word-wrap
 | 
					
						
							|  |  |  |         } else if (!currentLine.endsWith("-")) { | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |           currentLine += " "; | 
					
						
							|  |  |  |           currentLineWidthTillNow += spaceWidth; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         index++; | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         // Start appending words in a line till max width reached
 | 
					
						
							|  |  |  |         while (currentLineWidthTillNow < maxWidth && index < words.length) { | 
					
						
							|  |  |  |           const word = words[index]; | 
					
						
							|  |  |  |           currentLineWidthTillNow = getLineWidth(currentLine + word, font); | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-03 20:10:42 +08:00
										 |  |  |           if (currentLineWidthTillNow > maxWidth) { | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |             push(currentLine); | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  |             resetParams(); | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |             break; | 
					
						
							| 
									
										
										
										
											2022-01-03 20:29:26 +08:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |           index++; | 
					
						
							| 
									
										
										
										
											2023-04-20 13:40:46 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |           // if word ends with "-" then we don't need to add space
 | 
					
						
							|  |  |  |           // to sync with css word-wrap
 | 
					
						
							|  |  |  |           const shouldAppendSpace = !word.endsWith("-"); | 
					
						
							|  |  |  |           currentLine += word; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           if (shouldAppendSpace) { | 
					
						
							|  |  |  |             currentLine += " "; | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |           // Push the word if appending space exceeds max width
 | 
					
						
							|  |  |  |           if (currentLineWidthTillNow + spaceWidth >= maxWidth) { | 
					
						
							| 
									
										
										
										
											2023-04-20 13:40:46 +08:00
										 |  |  |             if (shouldAppendSpace) { | 
					
						
							|  |  |  |               lines.push(currentLine.slice(0, -1)); | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |               lines.push(currentLine); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  |             resetParams(); | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |             break; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  |     if (currentLine.slice(-1) === " ") { | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |       // only remove last trailing space which we have added when joining words
 | 
					
						
							| 
									
										
										
										
											2023-03-14 19:48:16 +08:00
										 |  |  |       currentLine = currentLine.slice(0, -1); | 
					
						
							| 
									
										
										
										
											2023-01-26 14:19:21 +08:00
										 |  |  |       push(currentLine); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |   }); | 
					
						
							|  |  |  |   return lines.join("\n"); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const charWidth = (() => { | 
					
						
							|  |  |  |   const cachedCharWidth: { [key: FontString]: Array<number> } = {}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const calculate = (char: string, font: FontString) => { | 
					
						
							|  |  |  |     const ascii = char.charCodeAt(0); | 
					
						
							|  |  |  |     if (!cachedCharWidth[font]) { | 
					
						
							|  |  |  |       cachedCharWidth[font] = []; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (!cachedCharWidth[font][ascii]) { | 
					
						
							| 
									
										
										
										
											2022-12-24 00:15:49 +08:00
										 |  |  |       const width = getLineWidth(char, font); | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |       cachedCharWidth[font][ascii] = width; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-02-18 20:50:55 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |     return cachedCharWidth[font][ascii]; | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const getCache = (font: FontString) => { | 
					
						
							|  |  |  |     return cachedCharWidth[font]; | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     calculate, | 
					
						
							|  |  |  |     getCache, | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | })(); | 
					
						
							| 
									
										
										
										
											2023-02-23 19:03:10 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  | const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // FIXME rename to getApproxMinContainerWidth
 | 
					
						
							|  |  |  | export const getApproxMinLineWidth = ( | 
					
						
							|  |  |  |   font: FontString, | 
					
						
							|  |  |  |   lineHeight: ExcalidrawTextElement["lineHeight"], | 
					
						
							|  |  |  | ) => { | 
					
						
							| 
									
										
										
										
											2022-02-18 20:50:55 +08:00
										 |  |  |   const maxCharWidth = getMaxCharWidth(font); | 
					
						
							|  |  |  |   if (maxCharWidth === 0) { | 
					
						
							| 
									
										
										
										
											2022-01-14 00:05:38 +08:00
										 |  |  |     return ( | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  |       measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width + | 
					
						
							| 
									
										
										
										
											2022-01-14 00:05:38 +08:00
										 |  |  |       BOUND_TEXT_PADDING * 2 | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2022-02-18 20:50:55 +08:00
										 |  |  |   return maxCharWidth + BOUND_TEXT_PADDING * 2; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const getMinCharWidth = (font: FontString) => { | 
					
						
							|  |  |  |   const cache = charWidth.getCache(font); | 
					
						
							|  |  |  |   if (!cache) { | 
					
						
							|  |  |  |     return 0; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const cacheWithOutEmpty = cache.filter((val) => val !== undefined); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return Math.min(...cacheWithOutEmpty); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-18 20:50:55 +08:00
										 |  |  | export const getMaxCharWidth = (font: FontString) => { | 
					
						
							|  |  |  |   const cache = charWidth.getCache(font); | 
					
						
							|  |  |  |   if (!cache) { | 
					
						
							|  |  |  |     return 0; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const cacheWithOutEmpty = cache.filter((val) => val !== undefined); | 
					
						
							|  |  |  |   return Math.max(...cacheWithOutEmpty); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | export const getApproxCharsToFitInWidth = (font: FontString, width: number) => { | 
					
						
							|  |  |  |   // Generally lower case is used so converting to lower case
 | 
					
						
							|  |  |  |   const dummyText = DUMMY_TEXT.toLocaleLowerCase(); | 
					
						
							|  |  |  |   const batchLength = 6; | 
					
						
							|  |  |  |   let index = 0; | 
					
						
							|  |  |  |   let widthTillNow = 0; | 
					
						
							|  |  |  |   let str = ""; | 
					
						
							|  |  |  |   while (widthTillNow <= width) { | 
					
						
							|  |  |  |     const batch = dummyText.substr(index, index + batchLength); | 
					
						
							|  |  |  |     str += batch; | 
					
						
							| 
									
										
										
										
											2022-12-24 00:15:49 +08:00
										 |  |  |     widthTillNow += getLineWidth(str, font); | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |     if (index === dummyText.length - 1) { | 
					
						
							|  |  |  |       index = 0; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     index = index + batchLength; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   while (widthTillNow > width) { | 
					
						
							|  |  |  |     str = str.substr(0, str.length - 1); | 
					
						
							| 
									
										
										
										
											2022-12-24 00:15:49 +08:00
										 |  |  |     widthTillNow = getLineWidth(str, font); | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  |   } | 
					
						
							|  |  |  |   return str.length; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const getBoundTextElementId = (container: ExcalidrawElement | null) => { | 
					
						
							| 
									
										
										
										
											2022-03-03 00:04:09 +08:00
										 |  |  |   return container?.boundElements?.length | 
					
						
							|  |  |  |     ? container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id || | 
					
						
							|  |  |  |         null | 
					
						
							|  |  |  |     : null; | 
					
						
							| 
									
										
										
										
											2021-12-16 23:44:03 +08:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2022-01-05 20:28:03 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | export const getBoundTextElement = (element: ExcalidrawElement | null) => { | 
					
						
							|  |  |  |   if (!element) { | 
					
						
							|  |  |  |     return null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const boundTextElementId = getBoundTextElementId(element); | 
					
						
							|  |  |  |   if (boundTextElementId) { | 
					
						
							| 
									
										
										
										
											2022-01-12 22:21:36 +08:00
										 |  |  |     return ( | 
					
						
							|  |  |  |       (Scene.getScene(element)?.getElement( | 
					
						
							|  |  |  |         boundTextElementId, | 
					
						
							|  |  |  |       ) as ExcalidrawTextElementWithContainer) || null | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return null; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const getContainerElement = ( | 
					
						
							|  |  |  |   element: | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |     | (ExcalidrawElement & { | 
					
						
							|  |  |  |         containerId: ExcalidrawElement["id"] | null; | 
					
						
							|  |  |  |       }) | 
					
						
							| 
									
										
										
										
											2022-01-12 22:21:36 +08:00
										 |  |  |     | null, | 
					
						
							|  |  |  | ) => { | 
					
						
							|  |  |  |   if (!element) { | 
					
						
							|  |  |  |     return null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (element.containerId) { | 
					
						
							|  |  |  |     return Scene.getScene(element)?.getElement(element.containerId) || null; | 
					
						
							| 
									
										
										
										
											2022-01-05 20:28:03 +08:00
										 |  |  |   } | 
					
						
							|  |  |  |   return null; | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2022-09-19 18:00:37 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  | export const getContainerCenter = ( | 
					
						
							|  |  |  |   container: ExcalidrawElement, | 
					
						
							|  |  |  |   appState: AppState, | 
					
						
							|  |  |  | ) => { | 
					
						
							|  |  |  |   if (!isArrowElement(container)) { | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       x: container.x + container.width / 2, | 
					
						
							|  |  |  |       y: container.y + container.height / 2, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const points = LinearElementEditor.getPointsGlobalCoordinates(container); | 
					
						
							|  |  |  |   if (points.length % 2 === 1) { | 
					
						
							|  |  |  |     const index = Math.floor(container.points.length / 2); | 
					
						
							|  |  |  |     const midPoint = LinearElementEditor.getPointGlobalCoordinates( | 
					
						
							|  |  |  |       container, | 
					
						
							|  |  |  |       container.points[index], | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |     return { x: midPoint[0], y: midPoint[1] }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const index = container.points.length / 2 - 1; | 
					
						
							|  |  |  |   let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints( | 
					
						
							|  |  |  |     container, | 
					
						
							|  |  |  |     appState, | 
					
						
							|  |  |  |   )[index]; | 
					
						
							|  |  |  |   if (!midSegmentMidpoint) { | 
					
						
							|  |  |  |     midSegmentMidpoint = LinearElementEditor.getSegmentMidPoint( | 
					
						
							|  |  |  |       container, | 
					
						
							|  |  |  |       points[index], | 
					
						
							|  |  |  |       points[index + 1], | 
					
						
							|  |  |  |       index + 1, | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] }; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  | export const getContainerCoords = (container: NonDeletedExcalidrawElement) => { | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |   let offsetX = BOUND_TEXT_PADDING; | 
					
						
							|  |  |  |   let offsetY = BOUND_TEXT_PADDING; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   if (container.type === "ellipse") { | 
					
						
							|  |  |  |     // The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172
 | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |     offsetX += (container.width / 2) * (1 - Math.sqrt(2) / 2); | 
					
						
							|  |  |  |     offsetY += (container.height / 2) * (1 - Math.sqrt(2) / 2); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   // The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6265
 | 
					
						
							|  |  |  |   if (container.type === "diamond") { | 
					
						
							|  |  |  |     offsetX += container.width / 4; | 
					
						
							|  |  |  |     offsetY += container.height / 4; | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   } | 
					
						
							|  |  |  |   return { | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |     x: container.x + offsetX, | 
					
						
							|  |  |  |     y: container.y + offsetY, | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   }; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  | export const getTextElementAngle = (textElement: ExcalidrawTextElement) => { | 
					
						
							|  |  |  |   const container = getContainerElement(textElement); | 
					
						
							|  |  |  |   if (!container || isArrowElement(container)) { | 
					
						
							|  |  |  |     return textElement.angle; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return container.angle; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const getBoundTextElementOffset = ( | 
					
						
							|  |  |  |   boundTextElement: ExcalidrawTextElement | null, | 
					
						
							|  |  |  | ) => { | 
					
						
							|  |  |  |   const container = getContainerElement(boundTextElement); | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   if (!container || !boundTextElement) { | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |     return 0; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (isArrowElement(container)) { | 
					
						
							|  |  |  |     return BOUND_TEXT_PADDING * 8; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |   return BOUND_TEXT_PADDING; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const getBoundTextElementPosition = ( | 
					
						
							|  |  |  |   container: ExcalidrawElement, | 
					
						
							|  |  |  |   boundTextElement: ExcalidrawTextElementWithContainer, | 
					
						
							|  |  |  | ) => { | 
					
						
							|  |  |  |   if (isArrowElement(container)) { | 
					
						
							|  |  |  |     return LinearElementEditor.getBoundTextElementPosition( | 
					
						
							|  |  |  |       container, | 
					
						
							|  |  |  |       boundTextElement, | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const shouldAllowVerticalAlign = ( | 
					
						
							|  |  |  |   selectedElements: NonDeletedExcalidrawElement[], | 
					
						
							|  |  |  | ) => { | 
					
						
							|  |  |  |   return selectedElements.some((element) => { | 
					
						
							|  |  |  |     const hasBoundContainer = isBoundToContainer(element); | 
					
						
							|  |  |  |     if (hasBoundContainer) { | 
					
						
							|  |  |  |       const container = getContainerElement(element); | 
					
						
							|  |  |  |       if (isTextElement(element) && isArrowElement(container)) { | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return true; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-03-14 19:51:46 +08:00
										 |  |  |     return false; | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const suppportsHorizontalAlign = ( | 
					
						
							|  |  |  |   selectedElements: NonDeletedExcalidrawElement[], | 
					
						
							|  |  |  | ) => { | 
					
						
							|  |  |  |   return selectedElements.some((element) => { | 
					
						
							|  |  |  |     const hasBoundContainer = isBoundToContainer(element); | 
					
						
							|  |  |  |     if (hasBoundContainer) { | 
					
						
							|  |  |  |       const container = getContainerElement(element); | 
					
						
							|  |  |  |       if (isTextElement(element) && isArrowElement(container)) { | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |         return false; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return true; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-03-14 19:51:46 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     return isTextElement(element); | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |   }); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-25 18:15:34 +08:00
										 |  |  | export const getTextBindableContainerAtPosition = ( | 
					
						
							|  |  |  |   elements: readonly ExcalidrawElement[], | 
					
						
							|  |  |  |   appState: AppState, | 
					
						
							|  |  |  |   x: number, | 
					
						
							|  |  |  |   y: number, | 
					
						
							|  |  |  | ): ExcalidrawTextContainer | null => { | 
					
						
							|  |  |  |   const selectedElements = getSelectedElements(elements, appState); | 
					
						
							|  |  |  |   if (selectedElements.length === 1) { | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |     return isTextBindableContainer(selectedElements[0], false) | 
					
						
							|  |  |  |       ? selectedElements[0] | 
					
						
							|  |  |  |       : null; | 
					
						
							| 
									
										
										
										
											2022-11-25 18:15:34 +08:00
										 |  |  |   } | 
					
						
							|  |  |  |   let hitElement = null; | 
					
						
							|  |  |  |   // We need to to hit testing from front (end of the array) to back (beginning of the array)
 | 
					
						
							|  |  |  |   for (let index = elements.length - 1; index >= 0; --index) { | 
					
						
							|  |  |  |     if (elements[index].isDeleted) { | 
					
						
							|  |  |  |       continue; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]); | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |     if ( | 
					
						
							|  |  |  |       isArrowElement(elements[index]) && | 
					
						
							| 
									
										
										
										
											2023-06-15 00:42:01 +08:00
										 |  |  |       isHittingElementNotConsideringBoundingBox( | 
					
						
							|  |  |  |         elements[index], | 
					
						
							|  |  |  |         appState, | 
					
						
							|  |  |  |         null, | 
					
						
							|  |  |  |         [x, y], | 
					
						
							|  |  |  |       ) | 
					
						
							| 
									
										
										
										
											2022-12-05 23:33:13 +08:00
										 |  |  |     ) { | 
					
						
							|  |  |  |       hitElement = elements[index]; | 
					
						
							|  |  |  |       break; | 
					
						
							|  |  |  |     } else if (x1 < x && x < x2 && y1 < y && y < y2) { | 
					
						
							| 
									
										
										
										
											2022-11-25 18:15:34 +08:00
										 |  |  |       hitElement = elements[index]; | 
					
						
							|  |  |  |       break; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return isTextBindableContainer(hitElement, false) ? hitElement : null; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-03 20:10:42 +08:00
										 |  |  | const VALID_CONTAINER_TYPES = new Set([ | 
					
						
							|  |  |  |   "rectangle", | 
					
						
							|  |  |  |   "ellipse", | 
					
						
							|  |  |  |   "diamond", | 
					
						
							|  |  |  |   "arrow", | 
					
						
							|  |  |  | ]); | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-09 19:11:15 +08:00
										 |  |  | export const isValidTextContainer = (element: { | 
					
						
							|  |  |  |   type: ExcalidrawElement["type"]; | 
					
						
							|  |  |  | }) => VALID_CONTAINER_TYPES.has(element.type); | 
					
						
							| 
									
										
										
										
											2023-03-03 20:10:42 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | export const computeContainerDimensionForBoundText = ( | 
					
						
							|  |  |  |   dimension: number, | 
					
						
							|  |  |  |   containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>, | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  | ) => { | 
					
						
							| 
									
										
										
										
											2023-03-03 20:10:42 +08:00
										 |  |  |   dimension = Math.ceil(dimension); | 
					
						
							|  |  |  |   const padding = BOUND_TEXT_PADDING * 2; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (containerType === "ellipse") { | 
					
						
							|  |  |  |     return Math.round(((dimension + padding) / Math.sqrt(2)) * 2); | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-03-03 20:10:42 +08:00
										 |  |  |   if (containerType === "arrow") { | 
					
						
							|  |  |  |     return dimension + padding * 8; | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-03-03 20:10:42 +08:00
										 |  |  |   if (containerType === "diamond") { | 
					
						
							|  |  |  |     return 2 * (dimension + padding); | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-03-03 20:10:42 +08:00
										 |  |  |   return dimension + padding; | 
					
						
							| 
									
										
										
										
											2023-02-21 15:06:43 +08:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-02 18:04:21 +08:00
										 |  |  | export const getBoundTextMaxWidth = ( | 
					
						
							|  |  |  |   container: ExcalidrawElement, | 
					
						
							|  |  |  |   boundTextElement: ExcalidrawTextElement | null = getBoundTextElement( | 
					
						
							|  |  |  |     container, | 
					
						
							|  |  |  |   ), | 
					
						
							|  |  |  | ) => { | 
					
						
							|  |  |  |   const { width } = container; | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |   if (isArrowElement(container)) { | 
					
						
							| 
									
										
										
										
											2023-08-02 18:04:21 +08:00
										 |  |  |     const minWidth = | 
					
						
							|  |  |  |       (boundTextElement?.fontSize ?? DEFAULT_FONT_SIZE) * | 
					
						
							|  |  |  |       ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO; | 
					
						
							|  |  |  |     return Math.max(ARROW_LABEL_WIDTH_FRACTION * width, minWidth); | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |   } | 
					
						
							|  |  |  |   if (container.type === "ellipse") { | 
					
						
							|  |  |  |     // The width of the largest rectangle inscribed inside an ellipse is
 | 
					
						
							|  |  |  |     // Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
 | 
					
						
							|  |  |  |     // equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172
 | 
					
						
							|  |  |  |     return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (container.type === "diamond") { | 
					
						
							|  |  |  |     // The width of the largest rectangle inscribed inside a rhombus is
 | 
					
						
							|  |  |  |     // Math.round(width / 2) - https://github.com/excalidraw/excalidraw/pull/6265
 | 
					
						
							|  |  |  |     return Math.round(width / 2) - BOUND_TEXT_PADDING * 2; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return width - BOUND_TEXT_PADDING * 2; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-25 20:36:23 +08:00
										 |  |  | export const getBoundTextMaxHeight = ( | 
					
						
							|  |  |  |   container: ExcalidrawElement, | 
					
						
							|  |  |  |   boundTextElement: ExcalidrawTextElementWithContainer, | 
					
						
							|  |  |  | ) => { | 
					
						
							| 
									
										
										
										
											2023-08-02 18:04:21 +08:00
										 |  |  |   const { height } = container; | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |   if (isArrowElement(container)) { | 
					
						
							|  |  |  |     const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2; | 
					
						
							|  |  |  |     if (containerHeight <= 0) { | 
					
						
							| 
									
										
										
										
											2023-04-25 20:36:23 +08:00
										 |  |  |       return boundTextElement.height; | 
					
						
							| 
									
										
										
										
											2023-02-22 18:58:12 +08:00
										 |  |  |     } | 
					
						
							|  |  |  |     return height; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (container.type === "ellipse") { | 
					
						
							|  |  |  |     // The height of the largest rectangle inscribed inside an ellipse is
 | 
					
						
							|  |  |  |     // Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from
 | 
					
						
							|  |  |  |     // equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172
 | 
					
						
							|  |  |  |     return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (container.type === "diamond") { | 
					
						
							|  |  |  |     // The height of the largest rectangle inscribed inside a rhombus is
 | 
					
						
							|  |  |  |     // Math.round(height / 2) - https://github.com/excalidraw/excalidraw/pull/6265
 | 
					
						
							|  |  |  |     return Math.round(height / 2) - BOUND_TEXT_PADDING * 2; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return height - BOUND_TEXT_PADDING * 2; | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2023-03-13 22:16:09 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | export const isMeasureTextSupported = () => { | 
					
						
							|  |  |  |   const width = getTextWidth( | 
					
						
							|  |  |  |     DUMMY_TEXT, | 
					
						
							|  |  |  |     getFontString({ | 
					
						
							|  |  |  |       fontSize: DEFAULT_FONT_SIZE, | 
					
						
							|  |  |  |       fontFamily: DEFAULT_FONT_FAMILY, | 
					
						
							|  |  |  |     }), | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  |   return width > 0; | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Unitless line height | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * In previous versions we used `normal` line height, which browsers interpret | 
					
						
							|  |  |  |  * differently, and based on font-family and font-size. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * To make line heights consistent across browsers we hardcode the values for | 
					
						
							|  |  |  |  * each of our fonts based on most common average line-heights. | 
					
						
							|  |  |  |  * See https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971
 | 
					
						
							|  |  |  |  * where the values come from. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | const DEFAULT_LINE_HEIGHT = { | 
					
						
							|  |  |  |   // ~1.25 is the average for Virgil in WebKit and Blink.
 | 
					
						
							|  |  |  |   // Gecko (FF) uses ~1.28.
 | 
					
						
							|  |  |  |   [FONT_FAMILY.Virgil]: 1.25 as ExcalidrawTextElement["lineHeight"], | 
					
						
							|  |  |  |   // ~1.15 is the average for Virgil in WebKit and Blink.
 | 
					
						
							|  |  |  |   // Gecko if all over the place.
 | 
					
						
							|  |  |  |   [FONT_FAMILY.Helvetica]: 1.15 as ExcalidrawTextElement["lineHeight"], | 
					
						
							|  |  |  |   // ~1.2 is the average for Virgil in WebKit and Blink, and kinda Gecko too
 | 
					
						
							|  |  |  |   [FONT_FAMILY.Cascadia]: 1.2 as ExcalidrawTextElement["lineHeight"], | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => { | 
					
						
							| 
									
										
										
										
											2023-03-30 03:16:23 +08:00
										 |  |  |   if (fontFamily in DEFAULT_LINE_HEIGHT) { | 
					
						
							| 
									
										
										
										
											2023-03-22 14:02:38 +08:00
										 |  |  |     return DEFAULT_LINE_HEIGHT[fontFamily]; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY]; | 
					
						
							|  |  |  | }; |