Remove aspect ratio

This commit is contained in:
Mathias Krafft 2025-03-28 09:33:11 +01:00
parent 89a733120f
commit 1919b3db1a
No known key found for this signature in database
GPG key ID: D99E394FA2319429

View file

@ -28,19 +28,21 @@ interface ShapeRecognitionResult {
} }
interface ShapeRecognitionOptions { interface ShapeRecognitionOptions {
closedDistThreshPercent: number; // Max distance between stroke start/end to consider shape closed shapeIsClosedPercentThreshold: number; // Max distance between stroke start/end to consider shape closed
cornerAngleThresh: number; // Angle (in degrees) below which a corner is considered "sharp" (for arrow detection) arrowTipAngleThreshold: number; // Angle (in degrees) below which a corner is considered "sharp" (for arrow detection)
rdpTolerancePercent: number; // RDP simplification tolerance (percentage of bounding box diagonal) rdpTolerancePercent: number; // RDP simplification tolerance (percentage of bounding box diagonal)
rectAngleThresh: number; // Angle (in degrees) to check for rectangle corners rectangleCornersAngleThreshold: number; // Angle (in degrees) to check for rectangle corners
rectOrientationThresh: number; // Angle difference (in degrees) to nearest 0/90 orientation to call it rectangle rectangleOrientationAngleThreshold: number; // Angle difference (in degrees) to nearest 0/90 orientation to call it rectangle
ellipseRadiusVarianceThreshold: number; // Variance in radius to consider a shape an ellipse
} }
const DEFAULT_OPTIONS: ShapeRecognitionOptions = { const DEFAULT_OPTIONS: ShapeRecognitionOptions = {
closedDistThreshPercent: 10, // distance between start/end < % of bounding box diagonal shapeIsClosedPercentThreshold: 20,
cornerAngleThresh: 60, // <60° considered a sharp corner (possible arrow tip) arrowTipAngleThreshold: 60,
rdpTolerancePercent: 10, // percentage of bounding box diagonal rdpTolerancePercent: 10,
rectAngleThresh: 20, // <20° considered a sharp corner (rectangle) rectangleCornersAngleThreshold: 20,
rectOrientationThresh: 10, // rectangleOrientationAngleThreshold: 10,
ellipseRadiusVarianceThreshold: 0.5
}; };
@ -67,16 +69,14 @@ export const recognizeShape = (
[boundingBox.maxX, boundingBox.maxY] as LocalPoint, [boundingBox.maxX, boundingBox.maxY] as LocalPoint,
) * options.rdpTolerancePercent / 100; ) * options.rdpTolerancePercent / 100;
const simplified = simplifyRDP(element.points, tolerance); const simplified = simplifyRDP(element.points, tolerance);
console.log("Simplified points:", simplified);
// Check if the original points form a closed shape
const start = element.points[0], end = element.points[element.points.length - 1]; const start = element.points[0], end = element.points[element.points.length - 1];
const closedDist = pointDistance(start, end); const closedDist = pointDistance(start, end);
const diag = Math.hypot(boundingBox.width, boundingBox.height); // diagonal of bounding box const boundingBoxDiagonal = Math.hypot(boundingBox.width, boundingBox.height);
const isClosed = closedDist < Math.max(10, diag * options.closedDistThreshPercent / 100); // e.g., threshold: 10px or % of size // e.g., threshold: 10px or % of size
console.log("Closed shape:", isClosed); const isClosed = closedDist < Math.max(10, boundingBoxDiagonal * options.shapeIsClosedPercentThreshold / 100);
let bestShape: Shape = 'freedraw'; // TODO: Should this even be possible in this mode? let bestShape: Shape = 'freedraw';
const boundingBoxCenter = getCenterForBounds([ const boundingBoxCenter = getCenterForBounds([
boundingBox.minX, boundingBox.minX,
@ -127,7 +127,7 @@ export const recognizeShape = (
console.log("Angles sum:", angles.reduce((a, b) => a + b, 0)); console.log("Angles sum:", angles.reduce((a, b) => a + b, 0));
// All angles are sharp enough, so we can check for rectangle/diamond // All angles are sharp enough, so we can check for rectangle/diamond
if (angles.every(a => (a > options.rectAngleThresh && a < 180 - options.rectAngleThresh))) { if (angles.every(a => (a > options.rectangleCornersAngleThreshold && a < 180 - options.rectangleCornersAngleThreshold))) {
// Determine orientation by checking the slope of each segment // Determine orientation by checking the slope of each segment
interface Segment { length: number; angleDeg: number; } interface Segment { length: number; angleDeg: number; }
const segments: Segment[] = []; const segments: Segment[] = [];
@ -148,7 +148,7 @@ export const recognizeShape = (
const angle = seg.angleDeg; const angle = seg.angleDeg;
const distToHoriz = Math.min(Math.abs(angle - 0), Math.abs(angle - 180)); const distToHoriz = Math.min(Math.abs(angle - 0), Math.abs(angle - 180));
const distToVert = Math.abs(angle - 90); const distToVert = Math.abs(angle - 90);
return (distToHoriz < options.rectOrientationThresh) || (distToVert < options.rectOrientationThresh); return (distToHoriz < options.rectangleOrientationAngleThreshold) || (distToVert < options.rectangleOrientationAngleThreshold);
}); });
if (hasAxisAlignedSide) { if (hasAxisAlignedSide) {
bestShape = "rectangle"; bestShape = "rectangle";
@ -157,26 +157,22 @@ export const recognizeShape = (
bestShape = "diamond"; bestShape = "diamond";
} }
} }
} else { } else if (isClosed) { // **Ellipse** (closed shape with few corners)
const aspectRatio = boundingBox.width && boundingBox.height ? Math.min(boundingBox.width, boundingBox.height) / Math.max(boundingBox.width, boundingBox.height) : 1; // Measure radius variance
// If aspect ratio ~1 (nearly square) and simplified has few corners, good for circle const cx = boundingBoxCenter[0];
if (aspectRatio > 0.8) { const cy = boundingBoxCenter[1];
// Measure radius variance let totalDist = 0, maxDist = 0, minDist = Infinity;
const cx = boundingBoxCenter[0]; for (const p of simplified) {
const cy = boundingBoxCenter[1]; const d = Math.hypot(p[0] - cx, p[1] - cy);
let totalDist = 0, maxDist = 0, minDist = Infinity; totalDist += d;
for (const p of simplified) { maxDist = Math.max(maxDist, d);
const d = Math.hypot(p[0] - cx, p[1] - cy); minDist = Math.min(minDist, d);
totalDist += d; }
maxDist = Math.max(maxDist, d); const avgDist = totalDist / simplified.length;
minDist = Math.min(minDist, d); const radiusVar = (maxDist - minDist) / (avgDist || 1);
} // If variance in radius is small, shape is round
const avgDist = totalDist / simplified.length; if (radiusVar < options.ellipseRadiusVarianceThreshold) {
const radiusVar = (maxDist - minDist) / (avgDist || 1); bestShape = 'ellipse';
// If variance in radius is small, shape is round
if (radiusVar < 0.3) {
bestShape = 'ellipse';
}
} }
} }