analyze($input); // full report data * $free = $engine->freeView($report); // score + top 3 issues (free) * * Input format: see docs/home-report-engine.md in the repository. * The PDF template (next build step) only reads $report; it never * recalculates anything. */ namespace VastuIQWP\Engine; defined( 'ABSPATH' ) || exit; final class Engine { private Analyzer $analyzer; public function __construct() { $this->analyzer = new Analyzer(new Rules(), new MagneticModel()); } /** Full analysis. Throws \InvalidArgumentException with a plain message for bad input. */ public function analyze(array $input): array { return $this->analyzer->analyze($input); } /** The free Vastu Score: overall score, band and the top 3 issues (no remedies). */ public function freeView(array $report): array { $top = array_map(fn($i) => ['title' => $i['title'], 'severity' => $i['severity'], 'floor' => $i['floor']], array_slice($report['issues'], 0, 3)); $entrance = null; foreach ($report['floors'] as $f) { if (!empty($f['entrance'])) { $entrance = $f['entrance']['direction_name']; break; } } $rooms = 0; $good = 0; foreach ($report['floors'] as $f) { $rooms += count($f['rooms']); foreach ($f['rooms'] as $r) if (($r['score'] ?? 0) >= 80) $good++; } return [ 'score' => $report['score']['overall'], 'band' => $report['score']['band'], 'top_issues' => $top, 'issues_total' => count($report['issues']), 'rooms_checked' => $rooms, 'location' => $report['location']['label'], 'hemisphere' => $report['location']['hemisphere'], 'true_north' => $report['true_north']['text'], 'sun_headline' => $report['sun']['facts'][1] ?? '', 'notices' => array_values(array_filter($report['notices'], fn($n) => in_array($n['id'], ['major_defect', 'southern_hemisphere', 'tropical', 'unmatched', 'no_entrance', 'directions_only'], true))), 'disclaimer' => $report['disclaimer'], 'declination' => $report['true_north']['declination'], 'rooms_good' => $good, 'input_source' => $report['input_source'], 'entrance' => $entrance, ]; } /** * "Quick score without a plan": the buyer says which direction each room is in * (as seen from the middle of the home). This builds the normal engine input: * a square home with true north straight up and each room at the middle of its zone. * Input: {location, property, floors:[{level, label?, rooms:[{name, direction}], entrance_direction?}]} */ public static function fromDirections(array $q): array { $dirs = ['N' => 0, 'NE' => 45, 'E' => 90, 'SE' => 135, 'S' => 180, 'SW' => 225, 'W' => 270, 'NW' => 315]; $floorsIn = array_slice(is_array($q['floors'] ?? null) ? $q['floors'] : [], 0, 4); if (!$floorsIn) throw new \InvalidArgumentException('Add at least one room'); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- message is never printed. $floors = []; $doorDone = false; foreach ($floorsIn as $f) { $rooms = []; foreach (array_slice(is_array($f['rooms'] ?? null) ? $f['rooms'] : [], 0, 30) as $r) { $name = trim(mb_substr((string)($r['name'] ?? ''), 0, 40)); $d = strtoupper((string)($r['direction'] ?? '')); if ($name === '') continue; if ($d === 'C') { $rooms[] = ['name' => $name, 'x' => 50.0, 'y' => 50.0]; continue; } if (!isset($dirs[$d])) throw new \InvalidArgumentException("Choose a direction for $name"); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- message is never printed. $b = deg2rad($dirs[$d]); $rooms[] = ['name' => $name, 'x' => round(50 + 33 * sin($b), 2), 'y' => round(50 - 33 * cos($b), 2)]; } if (!$rooms) continue; $floor = ['level' => $f['level'] ?? 'main', 'label' => (string)($f['label'] ?? ''), 'outline' => [[0, 0], [100, 0], [100, 100], [0, 100]], 'rooms' => $rooms]; $ed = strtoupper((string)($f['entrance_direction'] ?? '')); if (!$doorDone && isset($dirs[$ed])) { // on the outer wall, inside the chosen zone (diagonals sit a third of the way from the corner) $door = ['N' => [50, 0], 'NE' => [83, 0], 'E' => [100, 50], 'SE' => [100, 83], 'S' => [50, 100], 'SW' => [17, 100], 'W' => [0, 50], 'NW' => [0, 17]][$ed]; $floor['entrance'] = ['x' => $door[0], 'y' => $door[1]]; $doorDone = true; } $floors[] = $floor; } if (!$floors) throw new \InvalidArgumentException('Add at least one room'); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- message is never printed. return [ 'source' => 'directions', 'location' => $q['location'] ?? [], 'property' => $q['property'] ?? [], 'north' => ['image_bearing' => 0, 'reference' => 'true'], 'image' => ['width' => 1000, 'height' => 1000], 'floors' => $floors, ]; } /** Which room type each plan label is read as (for the confirm screen). Null = not recognised. */ public function matchNames(array $names): array { $rules = new Rules(); $out = []; foreach (array_slice($names, 0, 80) as $n) { $n = mb_substr((string)$n, 0, 40); $t = $rules->match($n); $out[] = ['name' => $n, 'type' => $t, 'label' => $t ? $rules->room($t)['label'] : null]; } return $out; } /** Room types for the "what is this room?" picker: [type => label]. */ public function roomTypes(): array { return array_map(fn($r) => $r['label'], (new Rules())->allRooms()); } }