['North' => ['NW corner', 'NE corner'], 'East' => ['NE corner', 'SE corner'], 'South' => ['SE corner', 'SW corner'], 'West' => ['SW corner', 'NW corner']], 'zone_by_index' => ['Transitional', 'Supportive', 'Balanced', 'Primary', 'Primary', 'Balanced', 'Supportive', 'Transitional'], 'zone_score' => ['Transitional' => 52, 'Supportive' => 70, 'Balanced' => 85, 'Primary' => 92], 'zone_rating' => ['Primary' => 'Excellent', 'Balanced' => 'Favourable', 'Supportive' => 'Neutral', 'Transitional' => 'Needs attention'], 'dir_base' => ['North' => 90, 'East' => 88, 'South' => 55, 'West' => 65], ]; private const PREFIX = ['North' => 'N', 'East' => 'E', 'South' => 'S', 'West' => 'W']; private array $P; private ?array $db = null; public function __construct() { $rules = json_decode(is_readable(dirname(__DIR__) . '/data/main-door-rules.json') ? (string)file_get_contents(dirname(__DIR__) . '/data/main-door-rules.json') : '', true); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reads a file bundled with the plugin. $this->P = (is_array($rules['position'] ?? null) ? $rules['position'] : []) + self::FALLBACK; $this->db = null; // door-position text is not used by the plugin } /** Score for a wall direction and a position along it (0–100 % from the first corner). */ public function score(string $wall, float $pct): array { $pct = max(0.0, min(99.999, $pct)); $idx = (int)floor($pct / 12.5); $zone = $this->P['zone_by_index'][$idx]; $within = fmod($pct, 12.5) / 12.5; $assess = (int)round(100 - abs($within - 0.5) * 2 * 45); $s = (int)round($this->P['dir_base'][$wall] * 0.5 + $this->P['zone_score'][$zone] * 0.35 + $assess * 0.15); return ['pada' => self::PREFIX[$wall] . ($idx + 1), 'index' => $idx, 'zone' => $zone, 'zone_rating' => $this->P['zone_rating'][$zone] ?? '', 'score' => $s]; } /** * Pada of the entrance from plan geometry. * $outlineLocal: house outline in the wall-aligned frame (x right, y down, centre at 0,0, aspect-corrected) * $doorLocal: the door in the same frame. $toTrue: callable(local direction angle in image terms) → true bearing. */ public function fromPlan(array $centre, array $outline, array $door, float $aspect, float $wallRotImg, float $trueNorthImg): ?array { if (count($outline) < 3) return null; $rot = deg2rad(-$wallRotImg); $toLocal = function (array $p) use ($rot, $aspect, $centre) { $x = ($p[0] - $centre[0]) * $aspect; $y = $p[1] - $centre[1]; return [$x * cos($rot) - $y * sin($rot), $x * sin($rot) + $y * cos($rot)]; }; $o = array_map($toLocal, $outline); [$minX, $minY, $maxX, $maxY] = Orientation::bbox($o); [$dx, $dy] = $toLocal($door); // the four walls of the (wall-aligned) footprint: [distance to door, outward normal in local frame, start, end] $walls = [ ['d' => abs($dy - $minY), 'n' => [0, -1], 'a' => [$minX, $minY], 'b' => [$maxX, $minY]], ['d' => abs($dx - $maxX), 'n' => [1, 0], 'a' => [$maxX, $minY], 'b' => [$maxX, $maxY]], ['d' => abs($dy - $maxY), 'n' => [0, 1], 'a' => [$maxX, $maxY], 'b' => [$minX, $maxY]], ['d' => abs($dx - $minX), 'n' => [-1, 0], 'a' => [$minX, $maxY], 'b' => [$minX, $minY]], ]; usort($walls, fn($p, $q) => $p['d'] <=> $q['d']); $w = $walls[0]; // local direction → image bearing → true bearing $localToTrue = function (array $v) use ($wallRotImg, $trueNorthImg) { $imgBearing = rad2deg(atan2($v[0], -$v[1])) + $wallRotImg; return Orientation::wrap($imgBearing - $trueNorthImg); }; $normal = $localToTrue($w['n']); $cardinals = ['North' => 0, 'East' => 90, 'South' => 180, 'West' => 270]; $wall = 'North'; $off = 999; foreach ($cardinals as $name => $deg) { $d = abs(fmod($normal - $deg + 540, 360) - 180); if ($d < $off) { $off = $d; $wall = $name; } } // pada 1 is at the end of the wall that lies toward (facing − 90°) $len = hypot($w['b'][0] - $w['a'][0], $w['b'][1] - $w['a'][1]); if ($len < 1e-6) return null; $u = [($w['b'][0] - $w['a'][0]) / $len, ($w['b'][1] - $w['a'][1]) / $len]; $alongTrue = $localToTrue($u); // direction from a to b $wantStart = Orientation::wrap($cardinals[$wall] - 90); // first corner side $dAB = abs(fmod($alongTrue - $wantStart + 540, 360) - 180); // small → b is the start $t = (($dx - $w['a'][0]) * $u[0] + ($dy - $w['a'][1]) * $u[1]) / $len; $t = max(0.0, min(1.0, $t)); $pct = ($dAB < 90 ? (1 - $t) : $t) * 100; $s = $this->score($wall, $pct); $better = []; for ($i = 0; $i < 8; $i++) { $alt = $this->score($wall, $i * 12.5 + 6.25); if ($alt['score'] > $s['score']) $better[] = $alt + ['from_pct' => $i * 12.5, 'to_pct' => ($i + 1) * 12.5]; } usort($better, fn($p, $q) => $q['score'] <=> $p['score']); $entry = $this->db[$s['pada']] ?? null; return $s + [ 'wall' => $wall, 'facing_bearing' => round($normal, 1), 'angled' => $off >= 15, // wall well off the compass: nearest direction used 'offset' => round($off, 1), 'position_pct' => round($pct, 1), 'corners' => $this->P['wall_order'][$wall], 'better' => array_slice($better, 0, 3), 'interpretation' => $entry['interpretation'] ?? null, 'strengths' => $entry['strengths'] ?? [], 'cautions' => $entry['cautions'] ?? [], ]; } }