All files markupShapeGeometry.js

76.53% Statements 75/98
44.44% Branches 8/18
83.33% Functions 5/6
76.53% Lines 75/98

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 991x 1x 1x 1x 4x 4x 4x 4x 4x 1x 1x 2x 1x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                               1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x  
export const MARKUP_DEFAULT_SHAPE = 'arrow';
export const MARKUP_DEFAULT_COLOR = '#ff0000';
export const MARKUP_PREVIEW_REFERENCE_DIM = 512;
 
function clampFraction(value) {
  const numeric = Number(value);
  if (!Number.isFinite(numeric)) return 0;
  return Math.max(0, Math.min(1, numeric));
}
 
export function sanitizeMarkupColor(color, fallback = MARKUP_DEFAULT_COLOR) {
  if (typeof color !== 'string') return fallback;
  const value = color.trim();
  return /^#[0-9a-fA-F]{6}$/.test(value) ? value.toLowerCase() : fallback;
}
 
export function sanitizeMarkupShape(
  shape,
  fallbackShape = MARKUP_DEFAULT_SHAPE,
  fallbackColor = MARKUP_DEFAULT_COLOR,
  fallbackWidth = 3,
) {
  if (!shape || typeof shape !== 'object') return null;
  const kind = ['line', 'rectangle', 'circle', 'arrow'].includes(shape.kind) ? shape.kind : fallbackShape;
  const x1 = clampFraction(shape.x1);
  const y1 = clampFraction(shape.y1);
  const x2 = clampFraction(shape.x2);
  const y2 = clampFraction(shape.y2);
  const width = Math.max(1, Math.min(64, Math.round(Number(shape.width) || fallbackWidth || 1)));
  return {
    kind,
    x1: Number(x1.toFixed(4)),
    y1: Number(y1.toFixed(4)),
    x2: Number(x2.toFixed(4)),
    y2: Number(y2.toFixed(4)),
    width,
    color: sanitizeMarkupColor(shape.color, fallbackColor),
  };
}
 
export function parseMarkupShapes(
  markupShapes,
  fallbackShape = MARKUP_DEFAULT_SHAPE,
  fallbackColor = MARKUP_DEFAULT_COLOR,
  fallbackWidth = 3,
) {
  if (Array.isArray(markupShapes)) {
    return markupShapes
      .map((shape) => sanitizeMarkupShape(shape, fallbackShape, fallbackColor, fallbackWidth))
      .filter(Boolean);
  }

  if (typeof markupShapes !== 'string' || !markupShapes.trim()) return [];

  try {
    const parsed = JSON.parse(markupShapes);
    if (!Array.isArray(parsed)) return [];
    return parsed
      .map((shape) => sanitizeMarkupShape(shape, fallbackShape, fallbackColor, fallbackWidth))
      .filter(Boolean);
  } catch {
    return [];
  }
}
 
export function getArrowGeometry(shape, imageWidth, imageHeight) {
  const x1 = shape.x1 * imageWidth;
  const y1 = shape.y1 * imageHeight;
  const x2 = shape.x2 * imageWidth;
  const y2 = shape.y2 * imageHeight;
  const dx = x2 - x1;
  const dy = y2 - y1;
  const length = Math.hypot(dx, dy) || 1;
  const ux = dx / length;
  const uy = dy / length;
  const strokeWidth = Math.max(1, shape.width);
  const headLength = Math.max(10, strokeWidth * 4);
  const headWidth = Math.max(8, strokeWidth * 3);
  const shaftX = x2 - ux * headLength;
  const shaftY = y2 - uy * headLength;
  const px = -uy;
  const py = ux;
  const leftX = shaftX + px * headWidth * 0.5;
  const leftY = shaftY + py * headWidth * 0.5;
  const rightX = shaftX - px * headWidth * 0.5;
  const rightY = shaftY - py * headWidth * 0.5;
  return {
    line: `${x1},${y1} ${shaftX},${shaftY}`,
    head: `${x2},${y2} ${leftX},${leftY} ${rightX},${rightY}`,
  };
}
 
export function getMarkupPreviewStrokeWidth(width, imageWidth, imageHeight) {
  const normalizedWidth = Math.max(1, Math.round(Number(width) || 1));
  const longestDim = Math.max(1, Number(imageWidth) || 0, Number(imageHeight) || 0);
  const scale = Math.max(1, longestDim / MARKUP_PREVIEW_REFERENCE_DIM);
  return Math.max(1, Math.round(normalizedWidth * scale));
}