self::coordinate( $value['x'] ), 'y' => self::coordinate( $value['y'] ), ); } /** * Saneert een genormaliseerde rechthoek en houdt hem binnen het beeld. * * @param mixed $value Ruwe invoer. * @return array{x: float, y: float, width: float, height: float}|null */ public static function rect( $value ): ?array { if ( ! is_array( $value ) ) { return null; } foreach ( array( 'x', 'y', 'width', 'height' ) as $key ) { if ( ! isset( $value[ $key ] ) || ! is_numeric( $value[ $key ] ) ) { return null; } } $x = self::coordinate( $value['x'] ); $y = self::coordinate( $value['y'] ); $w = self::coordinate( $value['width'] ); $h = self::coordinate( $value['height'] ); // Een rechthoek die kleiner is dan de minimummaat is geen constraint // maar ruis. Beter niets opslaan dan iets onbruikbaars. if ( $w < self::MIN_SAFE_SIZE || $h < self::MIN_SAFE_SIZE ) { return null; } // Houd de rechthoek binnen het beeld. Eerst de oorsprong terugduwen, // dan pas de maat inkorten — zo blijft de rechthoek zo groot mogelijk. if ( $x + $w > 1.0 ) { $x = max( 0.0, 1.0 - $w ); $w = min( $w, 1.0 - $x ); } if ( $y + $h > 1.0 ) { $y = max( 0.0, 1.0 - $h ); $h = min( $h, 1.0 - $y ); } return array( 'x' => $x, 'y' => $y, 'width' => $w, 'height' => $h, ); } /** * Saneert de lijst met secundaire focuspunten. * * In v1 worden deze opgeslagen en getoond, maar nog niet gewogen door de * engine. De datastructuur ligt nu al vast zodat dat later kan zonder * migratie. * * @param mixed $value Ruwe invoer. * @return array */ public static function points( $value ): array { if ( ! is_array( $value ) ) { return array(); } $out = array(); $seen = array(); foreach ( $value as $index => $raw ) { if ( count( $out ) >= self::MAX_POINTS ) { break; } $point = self::point( $raw ); if ( null === $point ) { continue; } $id = isset( $raw['id'] ) && is_string( $raw['id'] ) ? preg_replace( '/[^a-z0-9_-]/', '', strtolower( $raw['id'] ) ) : ''; if ( '' === $id ) { $id = 'point-' . ( (int) $index + 1 ); } // Dubbele id's zouden later stilzwijgend punten laten overschrijven. if ( isset( $seen[ $id ] ) ) { $id .= '-' . ( count( $out ) + 1 ); } $seen[ $id ] = true; $priority = isset( $raw['priority'] ) && is_numeric( $raw['priority'] ) ? (int) round( (float) $raw['priority'] ) : 50; $out[] = array( 'id' => $id, 'x' => $point['x'], 'y' => $point['y'], 'priority' => (int) max( 0, min( 100, $priority ) ), ); } return $out; } /** * Saneert de per-breakpoint overrides. * * Het datamodel ondersteunt ze vanaf v1; de interface toont ze nog niet. * Dat is een bewuste keuze: elke zichtbare instelling is een uitnodiging om * na te denken, en de belofte is juist "één keer instellen". * * @param mixed $value Ruwe invoer. * @return array{tablet: array|null, mobile: array|null} */ public static function breakpoints( $value ): array { $out = array( 'tablet' => null, 'mobile' => null, ); if ( ! is_array( $value ) ) { return $out; } foreach ( array_keys( $out ) as $key ) { if ( isset( $value[ $key ] ) ) { $out[ $key ] = self::point( $value[ $key ] ); } } return $out; } /** * Saneert het herkomstblok. * * @param mixed $value Ruwe invoer. * @return array{method: string, confidence: float|null, detectedAt: string|null} */ public static function source( $value ): array { $out = array( 'method' => 'manual', 'confidence' => null, 'detectedAt' => null, ); if ( ! is_array( $value ) ) { return $out; } if ( isset( $value['method'] ) && is_string( $value['method'] ) && in_array( $value['method'], Schema::source_methods(), true ) ) { $out['method'] = $value['method']; } if ( isset( $value['confidence'] ) && is_numeric( $value['confidence'] ) ) { $out['confidence'] = self::coordinate( $value['confidence'] ); } if ( isset( $value['detectedAt'] ) && is_string( $value['detectedAt'] ) ) { // ISO 8601, of niets. We slaan geen vrije tekst op in een datumveld. $stamp = substr( trim( $value['detectedAt'] ), 0, 32 ); if ( preg_match( '/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?)?/', $stamp ) ) { $out['detectedAt'] = $stamp; } } return $out; } /** * Normaliseert één coördinaat naar een float in [0, 1]. * * @param mixed $value Ruwe waarde. * @return float */ public static function coordinate( $value ): float { $number = (float) $value; if ( is_nan( $number ) || is_infinite( $number ) ) { return 0.0; } // Afronden op zes decimalen. Op een 8K-afbeelding is dat nog altijd // sub-pixelprecisie, en het houdt de opgeslagen JSON compact en // vergelijkbaar tussen server en browser. return round( Crop::clamp( $number, 0.0, 1.0 ), 6 ); } /** * Heeft dit object bruikbare focusdata? * * Een object zonder focuspunt én zonder safe area is leeg, ongeacht wat er * verder in staat. Dit is de test die bepaalt of de plugin überhaupt * ingrijpt op de frontend. * * @param array|null $data Gesaneerd focus-object. * @return bool */ public static function has_data( ?array $data ): bool { if ( ! is_array( $data ) ) { return false; } return ( null !== ( $data['focus'] ?? null ) ) || ( null !== ( $data['safeArea'] ?? null ) ); } }