Skip to content
Snippets Groups Projects
shapes.js 4.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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 vectorLength([x, y]) {
      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) / (vectorLength(p1) * vectorLength(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 = vectorLength(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