JamesNewton / HybridDiskEncoder

Hybrid Disk Encoder: A lasercut Analog / Quadrature encoder with more than 1 MILLION CPR
27 stars 5 forks source link

Rectangular slits make square eyes, not round. #3

Closed JamesNewton closed 5 years ago

JamesNewton commented 5 years ago

So the point of this is to get a round "eye" or circle when you plot the two sensors as X and Y on a graph. But that requires that the two sensors each put out a sin wave. It turns out that when the mask and slots are both rectangles, the resulting waveform is more of a triangle or a trapezoid, depending on their relative size.

Fig. 1: Rectangle passing over rectangle.

Fig. 1: Rectangle passing over circle.

The question of what shape results when you plot the area of intersection between two shapes out of phase on X and Y graph was interesting so I wrote a program to simulate it: https://stackblitz.com/edit/enc-slot-shape-area (change the code in any way to reload if you get an error about google not being defined). It's a bit of a mess because I started with rectangular shapes only and then added support for polygons to approximate any shape.

It's node.js so here is the package.json file

{
  "name": "enc-slot-shape-area",
  "version": "0.0.0",
  "private": true,
  "dependencies": {
    "google-charts": "2.0.0",
    "greiner-hormann": "1.4.1",
    "polygon-clipping": "^0.12.2"
  }
}

And the actual program, index.js

// Import stylesheets
import './style.css';
import {GoogleCharts} from 'google-charts';
import polygonClipping from 'polygon-clipping'

const poly1 = [[[0,0],[2,0],[2,2],[0,2],[0,0]]]
const poly2 = [[[0.1,0],[2.1,0],[2.1,2],[0.1,2],[0.1,0]]]

console.log(polygonClipping.intersection(poly1, poly2 /* , poly3, ... */))

// Write Javascript code!
const appDiv = document.getElementById('app');
//appDiv.innerHTML = `<h1>JS Starter</h1>`;
GoogleCharts.load('current', {packages: ['corechart', 'line']});
var data = new google.visualization.DataTable();
data.addColumn('number', 'X'); 
data.addColumn('number', 'Area');
//      data.addColumn('number', 'Cats');

function areaOverlapRect(shape1, shape2, inc) {
  var l1x=shape1.x+inc
  var l1y=shape1.y
  var r1x=shape1.x+shape1.w+inc
  var r1y=shape1.y+shape1.h
  var l2x=shape2.x
  var l2y=shape2.y
  var r2x=shape2.x+shape2.w
  var r2y=shape2.y+shape2.h
  var width = Math.max(Math.min(r1x, r2x) - Math.max(l1x, l2x),0)
  var height = Math.max(Math.min(r1y, r2y) - Math.max(l1y, l2y),0)
  return  width * height;
  }

//Area of polygon
//https://www.mathopenref.com/coordpolygonarea2.html

//Intersection of 2 polygons
//https://github.com/mfogel/polygon-clipping
function areaOverlapPoly(shape1, shape2, inc) {
  var total = 0
  var poly = [] 
  shape1.points.forEach((e,i) => {poly.push([e[0]+inc,e[1]])})
  var clip = polygonClipping.intersection([poly], [shape2.points] )
  //console.log(clip)
  if (!clip.length) {
    console.log("no intersection")
    return 0 
    }
  clip = clip[0][0] //polygon-clipping returns burried array
  for (var i = 0, l = clip.length-1; i <= l; i++) {
    var addX = clip[i][0];
    var addY = clip[i == l ? 0 : i + 1][1];
    var subX = clip[i == l ? 0 : i + 1][0];
    var subY = clip[i][1];

    total += (addX * addY * 0.5);
    total -= (subX * subY * 0.5);
    }
  //console.log(total)
  return Math.abs(total);
  }

function areaOverlap( shape1, shape2, inc) {
  if(shape1.type != shape2.type) { 
    console.log("only support same type")
    return null
  }
  if(shape1.type == "rect") return areaOverlapRect( shape1, shape2, inc)
  if(shape1.type == "poly") return areaOverlapPoly( shape1, shape2, inc)
  }

function drawPolyAtScaleCanvas(poly, x, y, scale, ctx) {
  ctx.beginPath();
  ctx.strokeStyle = poly.color;
  ctx.moveTo(scale*poly.points[0][0]+x, scale*poly.points[0][1]+y)
    for (var i in poly.points) { //re-does point 0 but who cares?
      ctx.lineTo(scale*poly.points[i][0]+x, scale*poly.points[i][1]+y)
      }
  ctx.stroke();
  }

function drawRectAtScaleCanvas(rect, x, y, scale, ctx) {
  ctx.beginPath();
  ctx.strokeStyle = rect.color;
  ctx.moveTo(scale*rect.x+x, scale*rect.y+y);
  ctx.lineTo(scale*(rect.x+rect.w)+x, scale*rect.y+y);
  ctx.lineTo(scale*(rect.x+rect.w)+x, scale*(rect.y+rect.h)+y);
  ctx.lineTo(scale*rect.x+x, scale*(rect.y+rect.h)+y);
  ctx.lineTo(scale*rect.x+x, scale*rect.y+y);
  ctx.stroke();
  }

function drawShapeAtScaleCanvas( shape, x, y, scale, ctx) {
  if(shape.type == "rect") return drawRectAtScaleCanvas(shape, x, y, scale, ctx)
  if(shape.type == "poly") return drawPolyAtScaleCanvas(shape, x, y, scale, ctx)
  }

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

var shape1 = {x:0, y:0, h:1, w:.5, color: "red", type: "rect"} 
var shape2 = {x:0, y:0, h:1, w:.2, color: "blue", type: "rect"}
var mask = 0.4
const poly = {points:[[0,0],[mask,0],[mask,1],[0,1],[0,0]], color:"red", type:"poly" }
var circles = {points:[], color:"black", type:"poly"}
for (var i = 0; i<Math.PI*2; i+=.5) {
  circles.points.push([(Math.cos(i)+1.001)/2,(Math.sin(i)+1.001)/2])
  }
circles.points.push(circles.points[0])
drawShapeAtScaleCanvas(circles, 30, 1, 10, ctx)
shape1 = poly
shape2 = circles
//console.log(areaOverlap( shape1, shape2, 0 ))
var viewScale = 10
var area 
var line = 0
for (var i=-1; i<2; i+=0.1) {
  drawShapeAtScaleCanvas(shape1, (i+1)*viewScale, line*viewScale, viewScale, ctx)
  drawShapeAtScaleCanvas(shape2, 1*viewScale, line*viewScale, viewScale, ctx)
  line += 1
  area = areaOverlap( shape1, shape2, i )
  //console.log( Math.round(area*1000)/1000 )
  data.addRows([[i,area]])
  }
var chart = new google.visualization.LineChart(document.getElementById('chart1'));
var options = {
  hAxis: {
    title: 'Offset'
  },
  vAxis: {
    title: 'Overlap'
  },
  series: {
    1: {curveType: 'function'}
  }
};
chart.draw(data, options);
JamesNewton commented 1 year ago

Actually, it turns out that a circle is not the ideal shape. It's much better, but not perfect. The perfect shape is actually sort of an inside out circle, as we found years ago, mid 2019.

image

var jw = { points: [], color: 'black', type: 'poly' };
for (var i = 0; i < Math.PI * 2; i += Math.PI / 8) {
  let x = i >= Math.PI ? Math.PI * 2 - i : i; //over to the right, then back
  x /= Math.PI * mask; //scale to the size of the mask
  jw.points.push([x, (Math.sin(i) + 1.0001) / 2]);
}
jw.points.push(jw.points[0]); //close the path

The difference is subtle and may not actually matter in operation, but it's been added in https://github.com/JamesNewton/HybridDiskEncoder/commit/6fc47649e3411f9941605c82fe5232267a1f0699