fix+docs v1.21.1: audit of v1.16-v1.21

- fix: Split snaps the click to the room's nearest wall instead of the grid, so
  it works on non-grid-aligned rooms (imported/legacy polygons); the pull is
  capped (~6 cells) so accidental mid-room clicks stay a miss; splitRoom() still
  rejects a bad cut. closestPointOnBoundary() + unit test.
- docs: README (en+ru) documents Merge/Split/ruler/per-space scale (were undocumented);
  TESTING.md merge/split rows + fresh self-run; new smokes smoke_merge_split,
  smoke_split_nonsnap (incl. the capped-pull case).
- 73 frontend + 12 backend tests green; module eval + both cards register.
This commit is contained in:
Matysh
2026-07-16 11:43:15 +03:00
parent b706ad4b49
commit 9288d74f6f
17 changed files with 414 additions and 145 deletions
+23
View File
@@ -105,6 +105,29 @@ function distToSeg(p: number[], a: number[], b: number[]): number {
* neighbouring rooms share walls, so their vertices sit on each other's outlines —
* including mid-span, since real walls overlap collinearly rather than match exactly.
*/
/**
* Project a point onto the nearest edge of a polygon and return that point,
* or null when the polygon has no edges. Used to snap a Split click onto the
* actual wall (rooms may not be grid-aligned — imported polygons, older configs),
* so cutting no longer requires hitting a grid node exactly on the outline.
*/
export function closestPointOnBoundary(p: number[], poly: number[][]): number[] | null {
if (!poly || poly.length < 2) return null;
let best: number[] | null = null;
let bestD = Infinity;
for (let i = 0; i < poly.length; i++) {
const a = poly[i], b = poly[(i + 1) % poly.length];
const dx = b[0] - a[0], dy = b[1] - a[1];
const len2 = dx * dx + dy * dy;
let t = len2 ? ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2 : 0;
t = Math.max(0, Math.min(1, t));
const q = [a[0] + t * dx, a[1] + t * dy];
const d = Math.hypot(p[0] - q[0], p[1] - q[1]);
if (d < bestD) { bestD = d; best = q; }
}
return best;
}
export function pointOnBoundary(p: number[], poly: number[][], eps = 1e-6): boolean {
if (!poly || poly.length < 2) return false;
for (let i = 0; i < poly.length; i++)