mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
room settings button: centroid pull breaks the plateau tie
The inscribed-circle criterion is FLAT along the long axis of any elongated room — every midline point fits the same circle — and a plain argmax took the first plateau sample: left of centre on the owner's kitchen-living room, above centre in the sauna. The score now subtracts a soft pull toward the area centroid (shoelace-weighted): on the plateau the nearest- to-centroid point wins, while real clearance differences still dominate, so the point never wanders into a thinner limb of an L. Verified on a replica of the owner's floor: kitchen slab centre within a few units, sauna dead-centre both axes. Units: wide and tall rectangles centre on both axes; the L keeps to its slab near the centroid x.
This commit is contained in:
File diff suppressed because one or more lines are too long
Vendored
+35
-35
File diff suppressed because one or more lines are too long
+28
-5
@@ -250,6 +250,24 @@ export function poleOfInaccessibility(poly: number[][], steps = 24): number[] {
|
|||||||
const ys = poly.map((p) => p[1]);
|
const ys = poly.map((p) => p[1]);
|
||||||
const minX = Math.min(...xs), maxX = Math.max(...xs);
|
const minX = Math.min(...xs), maxX = Math.max(...xs);
|
||||||
const minY = Math.min(...ys), maxY = Math.max(...ys);
|
const minY = Math.min(...ys), maxY = Math.max(...ys);
|
||||||
|
const span = Math.max(maxX - minX, maxY - minY) || 1;
|
||||||
|
// Area centroid (shoelace-weighted). Clearance alone has a PLATEAU on any
|
||||||
|
// elongated room — every point of the long midline fits the same circle —
|
||||||
|
// and a plain argmax took the first plateau point: left of centre on the
|
||||||
|
// owner's kitchen, above centre in the sauna. A soft pull toward the
|
||||||
|
// centroid breaks the tie along the plateau without ever dragging the
|
||||||
|
// point into a thinner limb (clearance differences dominate the score).
|
||||||
|
let a2 = 0, cx = 0, cy = 0;
|
||||||
|
for (let i = 0; i < poly.length; i++) {
|
||||||
|
const p = poly[i], q = poly[(i + 1) % poly.length];
|
||||||
|
const cross = p[0] * q[1] - q[0] * p[1];
|
||||||
|
a2 += cross;
|
||||||
|
cx += (p[0] + q[0]) * cross;
|
||||||
|
cy += (p[1] + q[1]) * cross;
|
||||||
|
}
|
||||||
|
const centroid = Math.abs(a2) > 1e-9
|
||||||
|
? [cx / (3 * a2), cy / (3 * a2)]
|
||||||
|
: [(minX + maxX) / 2, (minY + maxY) / 2];
|
||||||
const clearance = (x: number, y: number): number => {
|
const clearance = (x: number, y: number): number => {
|
||||||
if (!pointInPolygon([x, y], poly)) return -Infinity;
|
if (!pointInPolygon([x, y], poly)) return -Infinity;
|
||||||
let d = Infinity;
|
let d = Infinity;
|
||||||
@@ -259,14 +277,19 @@ export function poleOfInaccessibility(poly: number[][], steps = 24): number[] {
|
|||||||
}
|
}
|
||||||
return d;
|
return d;
|
||||||
};
|
};
|
||||||
|
const score = (x: number, y: number): number => {
|
||||||
|
const d = clearance(x, y);
|
||||||
|
if (d === -Infinity) return d;
|
||||||
|
return d - 0.08 * Math.hypot(x - centroid[0], y - centroid[1]) - 0.0001 * span;
|
||||||
|
};
|
||||||
let best: number[] | null = null;
|
let best: number[] | null = null;
|
||||||
let bestD = -Infinity;
|
let bestS = -Infinity;
|
||||||
for (let i = 1; i < steps; i++) {
|
for (let i = 1; i < steps; i++) {
|
||||||
for (let j = 1; j < steps; j++) {
|
for (let j = 1; j < steps; j++) {
|
||||||
const x = minX + ((maxX - minX) * i) / steps;
|
const x = minX + ((maxX - minX) * i) / steps;
|
||||||
const y = minY + ((maxY - minY) * j) / steps;
|
const y = minY + ((maxY - minY) * j) / steps;
|
||||||
const d = clearance(x, y);
|
const sc = score(x, y);
|
||||||
if (d > bestD) { bestD = d; best = [x, y]; }
|
if (sc > bestS) { bestS = sc; best = [x, y]; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (best) {
|
if (best) {
|
||||||
@@ -275,8 +298,8 @@ export function poleOfInaccessibility(poly: number[][], steps = 24): number[] {
|
|||||||
for (let i = -4; i <= 4; i++) {
|
for (let i = -4; i <= 4; i++) {
|
||||||
for (let j = -4; j <= 4; j++) {
|
for (let j = -4; j <= 4; j++) {
|
||||||
const x = bx + (cw * i) / 4, y = by + (ch * j) / 4;
|
const x = bx + (cw * i) / 4, y = by + (ch * j) / 4;
|
||||||
const d = clearance(x, y);
|
const sc = score(x, y);
|
||||||
if (d > bestD) { bestD = d; best = [x, y]; }
|
if (sc > bestS) { bestS = sc; best = [x, y]; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -917,12 +917,22 @@ test('poleOfInaccessibility: the VISUAL centre, not just any interior point', ()
|
|||||||
const p = poleOfInaccessibility(sq);
|
const p = poleOfInaccessibility(sq);
|
||||||
assert.ok(Math.abs(p[0] - 5) < 0.6 && Math.abs(p[1] - 5) < 0.6);
|
assert.ok(Math.abs(p[0] - 5) < 0.6 && Math.abs(p[1] - 5) < 0.6);
|
||||||
|
|
||||||
|
// an ELONGATED rectangle: clearance is a plateau along the midline, and a
|
||||||
|
// plain argmax used to take its first point — left of centre on the
|
||||||
|
// owner's kitchen, above centre in the sauna. The centroid pull centres it.
|
||||||
|
const wide = [[0, 0], [30, 0], [30, 10], [0, 10]];
|
||||||
|
const w = poleOfInaccessibility(wide);
|
||||||
|
assert.ok(Math.abs(w[0] - 15) < 1.2 && Math.abs(w[1] - 5) < 1.2, 'centred both axes: ' + w);
|
||||||
|
const tall = [[0, 0], [8, 0], [8, 28], [0, 28]];
|
||||||
|
const tl = poleOfInaccessibility(tall);
|
||||||
|
assert.ok(Math.abs(tl[0] - 4) < 1 && Math.abs(tl[1] - 14) < 1.2, 'sauna case, centred vertically: ' + tl);
|
||||||
|
|
||||||
// the owner's kitchen-living room shape: a THICK top slab with a narrower
|
// the owner's kitchen-living room shape: a THICK top slab with a narrower
|
||||||
// column hanging off the right side. The button belongs in the middle of
|
// column hanging off the right side. The button belongs near the middle of
|
||||||
// the slab — the widest open space — not near the seam or down the column.
|
// the slab — pulled toward the area centroid, never down the thin column.
|
||||||
const L = [[0, 0], [20, 0], [20, 16], [16, 16], [16, 8], [0, 8]];
|
const L = [[0, 0], [20, 0], [20, 16], [16, 16], [16, 8], [0, 8]];
|
||||||
const q = poleOfInaccessibility(L);
|
const q = poleOfInaccessibility(L);
|
||||||
assert.ok(pointInPolygon(q, L), 'inside, always');
|
assert.ok(pointInPolygon(q, L), 'inside, always');
|
||||||
assert.ok(q[1] < 8, 'in the thick slab (y < 8), not down the column: ' + q);
|
assert.ok(q[1] < 8, 'in the thick slab (y < 8), not down the column: ' + q);
|
||||||
assert.ok(q[0] > 2 && q[0] < 16, 'horizontally inside the slab body: ' + q);
|
assert.ok(Math.abs(q[0] - 11.3) < 2.5, 'near the centroid x within the slab: ' + q);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user