Skip to content
Snippets Groups Projects
shapes.js 4.7 KiB
Newer Older
const LINE_ANGLE_THRESHOLD = Math.PI / 6
const VECTOR_LEN_THRESHOLD_FRACTION = 0.3
const RECT_MATRIX_SIZE = 3
const RECT_MATRIX_CENTER_RATIO = 0.65
const RECT_THRESHOLD_CENTER = 0
const RECT_THRESHOLD_SIDE_VARIANCE = 0.25
const MIN_RECT_POINTS = 4
const MIN_LINE_POINTS = 2

function getDistance(p1, p2) {
  if (!(p1 && p2)) return 0
  const [[x0, y0], [x1, y1]] = [p1, p2]
  return Math.hypot(x1 - x0, y1 - y0)
function vectorLen(v) {
  const [x, y] = v
  return Math.hypot(x, y)
}

function diffVector([x0, y0], [x1, y1]) {
  return [x0 - x1, y0 - y1]
}

function angleBetweenVectors(p1, p2) {
  const [[x0, y0], [x1, y1]] = [p1, p2]
  return Math.acos((x0 * x1 + y0 * y1) / (vectorLen(p1) * vectorLen(p2)))
function boundingCoords(points) {
  const xs = points.map((p) => p[0])
  const ys = points.map((p) => p[1])
  return {
    maxX: Math.max(...xs),
    minX: Math.min(...xs),
    maxY: Math.max(...ys),
    minY: Math.min(...ys),
function matrixBoundsArray(min, max) {
  const d = max - min
  const centerSegmentSize = d * RECT_MATRIX_CENTER_RATIO
  const smallStep = (d - centerSegmentSize) / 2
  const p = [min + smallStep, min + smallStep + centerSegmentSize, max]
  return p
}

function getCluster([x, y], xBounds, yBounds) {
  return {
    x: xBounds.findIndex((bound) => x <= bound),
    y: yBounds.findIndex((bound) => y <= bound),
  }
}

function computeClusters(points, xBounds, yBounds) {
  const clusters = Array(RECT_MATRIX_SIZE)
      Array(RECT_MATRIX_SIZE)
        .map(() => ({ arr: [], sum: 0 })),
  const intervals = points.map((point, i) => ({
    point,
    dist: getDistance(point, points[i + 1]),
  }))

  let totalSum = 0
  intervals.forEach((interval) => {
    const { x, y } = getCluster(interval.point, xBounds, yBounds)
    clusters[x][y].arr.push(interval)
    clusters[x][y].sum += interval.dist
    totalSum += interval.dist
  return { arr: clusters, totalSum }
function clusterCoefficients(clusters) {
  return clusters.arr.map((rowCluster) =>
    rowCluster.map((cluster) => cluster.sum / clusters.totalSum),
export function computeMatrixCoefficients(points, boundingRect) {
  const { maxX, minX, maxY, minY } = boundingRect
  const xBounds = matrixBoundsArray(minX, maxX)
  const yBounds = matrixBoundsArray(minY, maxY)
  const clusters = computeClusters(points, xBounds, yBounds)
  const coefficients = clusterCoefficients(clusters, points)
  return coefficients
}

function couldBeRect(points) {
  if (points.length < MIN_RECT_POINTS) return false

  const boundingRect = boundingCoords(points)
  const matrixCoefficients = computeMatrixCoefficients(points, boundingRect)

  let [maxC, minC] = [0, 1]
  for (let i = 0; i < RECT_MATRIX_SIZE; i++) {
    for (let j = 0; j < RECT_MATRIX_SIZE; j++) {
      if (!(i === j && j === 1)) {
        maxC = Math.max(maxC, matrixCoefficients[i][j])
        minC = Math.min(minC, matrixCoefficients[i][j])
      }
    }
  }

  if (
    matrixCoefficients[1][1] <= RECT_THRESHOLD_CENTER &&
    maxC - minC < RECT_THRESHOLD_SIDE_VARIANCE
    return { coefficients: matrixCoefficients, boundingRect }
  return undefined
function couldBeLine(points) {
  if (points.length < MIN_LINE_POINTS) return false

  const vectorThreshold = Math.floor(
    points.length * VECTOR_LEN_THRESHOLD_FRACTION,
  )
  const pivot = points[0]
  let cumulativeThreshold = 0
  for (let i = 2; i < points.length; i++) {
    const prev = points[i - 1]
    const curr = points[i]
    const d1 = diffVector(pivot, prev)
    const d2 = diffVector(prev, curr)
    const angle = angleBetweenVectors(d1, d2)
    if (Math.abs(angle) > LINE_ANGLE_THRESHOLD) {
      const d2Len = vectorLen(d2)
      if (cumulativeThreshold < vectorThreshold && d2Len < vectorThreshold) {
        cumulativeThreshold += d2Len
        continue
      }
      return false
    }
  }
  return true
}

function recognizedRect(_, rectDetectionData) {
  const { minX, minY, maxX, maxY } = rectDetectionData.boundingRect
  return {
    boundingPoints: [
      [minX, minY],
      [minX, maxY],
      [maxX, maxY],
      [maxX, minY],
      [minX, minY],
    ],
    shape: Shapes.rectangle,
  }
function recognizedLine(points) {
  const [p1, p2] = [points[0], points[points.length - 1]]
  return {
    shape: Shapes.line,

    // Take only [x, y] from the whole point tuple
    lastPoint: p2.slice(0, 2),
    firstPoint: p1.slice(0, 2),
function recognizeFromPoints(points) {
  const rectDetectData = couldBeRect(points)
  if (rectDetectData) {
    return recognizedRect(points, rectDetectData)
  } else if (couldBeLine(points)) {
    return recognizedLine(points)
Yuriy Maksymets's avatar
Yuriy Maksymets committed
export const Shapes = {
  rectangle: "rect",
  line: "line",
Yuriy Maksymets's avatar
Yuriy Maksymets committed
}

export default recognizeFromPoints