*/ private array $cache = array(); /** * Leest de focusdata van een attachment. * * @param int $attachment_id Attachment-ID. * @return array|null Gesaneerd focus-object, of null als er niets is. */ public function get( int $attachment_id ): ?array { if ( $attachment_id <= 0 ) { return null; } if ( array_key_exists( $attachment_id, $this->cache ) ) { return $this->cache[ $attachment_id ]; } $raw = get_post_meta( $attachment_id, Naming::META_KEY, true ); if ( ! is_array( $raw ) || array() === $raw ) { $this->cache[ $attachment_id ] = null; return null; } $data = Sanitizer::focus( $raw ); if ( ! Sanitizer::has_data( $data ) ) { $this->cache[ $attachment_id ] = null; return null; } /** * Filtert de focusdata direct na het lezen. * * Hiermee kan een thema of plugin focusdata leveren voor afbeeldingen * die hem nog niet hebben, of een bestaande waarde overschrijven. * * @param array $data Gesaneerd focus-object. * @param int $attachment_id Attachment-ID. */ $data = apply_filters( 'yinga_focus/get', $data, $attachment_id ); $this->cache[ $attachment_id ] = $data; return $data; } /** * Schrijft focusdata weg. * * Vervangt het hele object. Voor een gedeeltelijke wijziging bestaat * patch(), die read-modify-write server-side afhandelt. * * @param int $attachment_id Attachment-ID. * @param array $data Ruwe of gesaneerde data. * @return array Het weggeschreven, gesaneerde object. */ public function save( int $attachment_id, array $data ): array { $clean = Sanitizer::focus( $data ); /** * Filtert focusdata vlak vóór het opslaan. * * @param array $clean Gesaneerd focus-object. * @param int $attachment_id Attachment-ID. */ $clean = apply_filters( 'yinga_focus/pre_save', $clean, $attachment_id ); update_post_meta( $attachment_id, Naming::META_KEY, $clean ); $this->cache[ $attachment_id ] = Sanitizer::has_data( $clean ) ? $clean : null; /** * Vuurt nadat focusdata is opgeslagen. * * @param int $attachment_id Attachment-ID. * @param array $clean Het opgeslagen object. */ do_action( 'yinga_focus/saved', $attachment_id, $clean ); return $clean; } /** * Werkt een deel van het object bij, zonder de rest te wissen. * * Dit is de reden dat we naast de meta-endpoint een eigen REST-route * hebben: REST-meta-writes vervangen het HELE object, dus een partiële * update via /wp/v2/media zou de rest wegvagen tenzij de client * read-modify-write doet. Hier gebeurt dat server-side en atomisch. * * @param int $attachment_id Attachment-ID. * @param array $patch Alleen de te wijzigen sleutels op topniveau. * @return array Het weggeschreven object. */ public function patch( int $attachment_id, array $patch ): array { $current = $this->get( $attachment_id ) ?? Schema::blank(); foreach ( $patch as $key => $value ) { if ( array_key_exists( $key, $current ) ) { $current[ $key ] = $value; } } return $this->save( $attachment_id, $current ); } /** * Wist de focusdata. * * @param int $attachment_id Attachment-ID. * @return bool Of er iets is verwijderd. */ public function delete( int $attachment_id ): bool { $deleted = delete_post_meta( $attachment_id, Naming::META_KEY ); $this->cache[ $attachment_id ] = null; /** * Vuurt nadat focusdata is gewist. * * @param int $attachment_id Attachment-ID. */ do_action( 'yinga_focus/deleted', $attachment_id ); return (bool) $deleted; } /** * Heeft deze attachment focusdata? * * @param int $attachment_id Attachment-ID. * @return bool */ public function has( int $attachment_id ): bool { return null !== $this->get( $attachment_id ); } /** * Het focuspunt, met terugval op het midden. * * @param int $attachment_id Attachment-ID. * @return array{x: float, y: float} */ public function focus( int $attachment_id ): array { $data = $this->get( $attachment_id ); return $data['focus'] ?? Schema::neutral_focus(); } /** * De intrinsieke afmetingen van een attachment. * * De crop-engine heeft ze nodig; de frontend-runtime krijgt ze mee zodat * hij niet op `naturalWidth` hoeft te wachten. * * @param int $attachment_id Attachment-ID. * @return array{width: int, height: int}|null */ public function dimensions( int $attachment_id ): ?array { $meta = wp_get_attachment_metadata( $attachment_id ); if ( ! is_array( $meta ) || empty( $meta['width'] ) || empty( $meta['height'] ) ) { return null; } return array( 'width' => (int) $meta['width'], 'height' => (int) $meta['height'], ); } /** * De CSS-waarde voor tier 1: het focuspunt zelf. * * Aspectratio-onafhankelijk, en daarmee server-side te bepalen zonder de * containerverhouding te kennen. Het focuspunt valt hiermee nooit buiten * beeld; wat het niet garandeert is dat de omgeving meekomt. * * @param int $attachment_id Attachment-ID. * @return string|null Bijvoorbeeld "62% 28%", of null zonder focusdata. */ public function css_position( int $attachment_id ): ?string { $data = $this->get( $attachment_id ); if ( null === $data || null === $data['focus'] ) { return null; } return Crop::focus_to_css( $data['focus'] ); } /** * De exacte positie voor een bekende containerverhouding — tier 2. * * Zodra de doelverhouding bekend is (een aspect ratio of vaste hoogte in de * builder), kan de safe-area-optimale positie server-side worden berekend. * Nog steeds nul JavaScript, nu mét safe-area-garantie. * * @param int $attachment_id Attachment-ID. * @param float $box_w Breedte van de container (of een verhouding). * @param float $box_h Hoogte van de container. * @return array|null Uitkomst van de engine, of null zonder focus- of beeldgegevens. */ public function resolve( int $attachment_id, float $box_w, float $box_h ): ?array { $data = $this->get( $attachment_id ); $dims = $this->dimensions( $attachment_id ); if ( null === $data || null === $dims ) { return null; } return Crop::object_position( (float) $dims['width'], (float) $dims['height'], $box_w, $box_h, $data['focus'] ?? Schema::neutral_focus(), $data['safeArea'], $data['textZone'] ?? null ); } /** * Leegt de request-cache. Vooral nuttig in tests en WP-CLI-loops. * * @param int|null $attachment_id Eén attachment, of null voor alles. */ public function flush( ?int $attachment_id = null ): void { if ( null === $attachment_id ) { $this->cache = array(); return; } unset( $this->cache[ $attachment_id ] ); } }