<?php
/**
 * Row detection & column clustering.
 *
 * Converts Figma's tree (auto-layout or absolute-positioned) into logical
 * rows and columns with percentage-based widths.
 */

if ( ! defined( 'ABSPATH' ) ) exit;

class FTE_Row_Detector {

    private float $overlap_threshold;
    private int   $max_columns;

    public function __construct( float $overlap_threshold = 0.5, int $max_columns = 10 ) {
        $this->overlap_threshold = $overlap_threshold;
        $this->max_columns       = $max_columns;
    }

    // -----------------------------------------------------------------------
    // Row detection
    // -----------------------------------------------------------------------

    /**
     * Detect rows from a Figma node's children.
     *
     * @param array $parent Figma node.
     * @return array[] Each row is [ 'children' => [...], 'boundingBox' => [...] ]
     */
    public function detect_rows( array $parent ): array {
        $children = $this->get_visible_children( $parent );
        if ( empty( $children ) ) return [];

        $layout_mode = $parent['layoutMode'] ?? 'NONE';

        switch ( $layout_mode ) {
            case 'VERTICAL':
                return $this->detect_rows_vertical( $parent, $children );
            case 'HORIZONTAL':
                return [ $this->wrap_as_one_row( $children ) ];
            default:
                return $this->detect_rows_absolute( $children );
        }
    }

    /**
     * Split a detected row into columns with percentage widths.
     *
     * @param array $row    A detected row.
     * @param array $parent The parent Figma node.
     * @return array[] Each col is [ 'node' => ..., 'widthPct' => int ]
     */
    public function detect_columns( array $row, array $parent ): array {
        $children = $row['children'];
        if ( empty( $children ) ) return [];

        if ( count( $children ) === 1 ) {
            return [ [ 'node' => $children[0], 'widthPct' => 100 ] ];
        }

        // Sort left-to-right
        usort( $children, function( $a, $b ) {
            return ( $a['absoluteBoundingBox']['x'] ?? 0 ) <=> ( $b['absoluteBoundingBox']['x'] ?? 0 );
        });

        $parent_box  = $parent['absoluteBoundingBox'];
        $layout_mode = $parent['layoutMode'] ?? 'NONE';

        if ( $layout_mode === 'HORIZONTAL' ) {
            $raw_widths = $this->compute_auto_layout_widths( $parent, $children );
        } else {
            $raw_widths = $this->compute_absolute_widths( $parent_box, $children );
        }

        $percentages = $this->normalize_to_percentages( $raw_widths );

        $columns = [];
        foreach ( $children as $i => $child ) {
            $columns[] = [
                'node'     => $child,
                'widthPct' => $percentages[ $i ],
            ];
        }
        return $columns;
    }

    // -----------------------------------------------------------------------
    // Row detection strategies
    // -----------------------------------------------------------------------

    private function detect_rows_vertical( array $parent, array $children ): array {
        $rows = [];
        foreach ( $children as $child ) {
            if ( ( $child['layoutPositioning'] ?? '' ) === 'ABSOLUTE' ) continue;
            $rows[] = [
                'children'    => [ $child ],
                'boundingBox' => $child['absoluteBoundingBox'] ?? $parent['absoluteBoundingBox'],
            ];
        }
        return $rows;
    }

    private function wrap_as_one_row( array $children ): array {
        $boxes = array_filter( array_map( fn( $c ) => $c['absoluteBoundingBox'] ?? null, $children ) );
        if ( empty( $boxes ) ) {
            return [ 'children' => $children, 'boundingBox' => [ 'x' => 0, 'y' => 0, 'width' => 0, 'height' => 0 ] ];
        }
        $min_x = min( array_column( $boxes, 'x' ) );
        $min_y = min( array_column( $boxes, 'y' ) );
        $max_x = max( array_map( fn( $b ) => $b['x'] + $b['width'], $boxes ) );
        $max_y = max( array_map( fn( $b ) => $b['y'] + $b['height'], $boxes ) );

        return [
            'children'    => $children,
            'boundingBox' => [ 'x' => $min_x, 'y' => $min_y, 'width' => $max_x - $min_x, 'height' => $max_y - $min_y ],
        ];
    }

    /**
     * Y-coordinate sweep-line clustering for absolutely positioned children.
     */
    private function detect_rows_absolute( array $children ): array {
        usort( $children, function( $a, $b ) {
            return ( $a['absoluteBoundingBox']['y'] ?? 0 ) <=> ( $b['absoluteBoundingBox']['y'] ?? 0 );
        });

        $clusters = [];
        $bounds   = [];

        foreach ( $children as $child ) {
            $child_box = $child['absoluteBoundingBox'];
            $merged    = false;

            if ( ! empty( $clusters ) ) {
                $last_idx   = count( $clusters ) - 1;
                $last_bound = $bounds[ $last_idx ];

                $overlap   = $this->vertical_overlap( $last_bound, $child_box );
                $min_h     = min( $last_bound['height'], $child_box['height'] );

                if ( $min_h > 0 && $overlap / $min_h >= $this->overlap_threshold ) {
                    $clusters[ $last_idx ][] = $child;
                    $bounds[ $last_idx ]     = $this->expand_bounds( $last_bound, $child_box );
                    $merged = true;
                }
            }

            if ( ! $merged ) {
                $clusters[] = [ $child ];
                $bounds[]   = $child_box;
            }
        }

        return array_map( function( $cluster, $i ) use ( $bounds ) {
            return [ 'children' => $cluster, 'boundingBox' => $bounds[ $i ] ];
        }, $clusters, array_keys( $clusters ) );
    }

    // -----------------------------------------------------------------------
    // Column width computation
    // -----------------------------------------------------------------------

    private function compute_auto_layout_widths( array $parent, array $sorted_children ): array {
        $parent_box    = $parent['absoluteBoundingBox'];
        $pad_l         = $parent['paddingLeft'] ?? 0;
        $pad_r         = $parent['paddingRight'] ?? 0;
        $gap           = $parent['itemSpacing'] ?? 0;
        $content_width = $parent_box['width'] - $pad_l - $pad_r;
        $total_gaps    = $gap * max( 0, count( $sorted_children ) - 1 );

        $total_grow = 0;
        $fixed_space = 0;
        foreach ( $sorted_children as $c ) {
            $grow = $c['layoutGrow'] ?? 0;
            $total_grow += $grow;
            if ( $grow == 0 ) {
                $fixed_space += $c['absoluteBoundingBox']['width'] ?? 0;
            }
        }

        $flex_space = max( 0, $content_width - $fixed_space - $total_gaps );

        $widths = [];
        foreach ( $sorted_children as $c ) {
            $grow = $c['layoutGrow'] ?? 0;
            if ( $grow > 0 && $total_grow > 0 ) {
                $widths[] = ( $flex_space * $grow ) / $total_grow;
            } else {
                $widths[] = $c['absoluteBoundingBox']['width'] ?? 0;
            }
        }
        return $widths;
    }

    private function compute_absolute_widths( array $parent_box, array $sorted_children ): array {
        // Use each child's actual rendered width as its raw column width.
        // The old implementation used x_next - x_current (spans) which collapsed
        // columns to tiny slivers when Figma children were not tiled edge-to-edge,
        // forcing downstream widget wrap and massive vertical inflation.
        $widths = array_map(
            fn( $c ) => max( 1.0, (float) ( $c['absoluteBoundingBox']['width'] ?? 0 ) ),
            $sorted_children
        );

        // Absorb any leading slack (parent left edge to first child) into col[0]
        // so the row visually starts where the parent does.
        if ( ! empty( $sorted_children ) ) {
            $first_x = $sorted_children[0]['absoluteBoundingBox']['x'] ?? $parent_box['x'];
            $left_margin = $first_x - $parent_box['x'];
            if ( $left_margin > 1 ) {
                $widths[0] += $left_margin;
            }
        }

        return $widths;
    }

    // -----------------------------------------------------------------------
    // Percentage normalization (largest-remainder method)
    // -----------------------------------------------------------------------

    /**
     * @param float[] $raw_widths
     * @return int[] Percentages summing to exactly 100.
     */
    public function normalize_to_percentages( array $raw_widths ): array {
        if ( empty( $raw_widths ) ) return [];
        if ( count( $raw_widths ) === 1 ) return [ 100 ];

        // Merge if too many columns
        while ( count( $raw_widths ) > $this->max_columns ) {
            $min_sum = PHP_FLOAT_MAX;
            $min_idx = 0;
            for ( $i = 0; $i < count( $raw_widths ) - 1; $i++ ) {
                $sum = $raw_widths[ $i ] + $raw_widths[ $i + 1 ];
                if ( $sum < $min_sum ) { $min_sum = $sum; $min_idx = $i; }
            }
            $merged = $raw_widths[ $min_idx ] + $raw_widths[ $min_idx + 1 ];
            array_splice( $raw_widths, $min_idx, 2, [ $merged ] );
        }

        $total = array_sum( $raw_widths );
        if ( $total == 0 ) {
            $each   = (int) floor( 100 / count( $raw_widths ) );
            $result = array_fill( 0, count( $raw_widths ), $each );
            $result[ count( $result ) - 1 ] += 100 - $each * count( $raw_widths );
            return $result;
        }

        $exact   = array_map( fn( $w ) => ( $w / $total ) * 100, $raw_widths );
        $floored = array_map( 'intval', array_map( 'floor', $exact ) );
        $remainders = [];
        for ( $i = 0; $i < count( $exact ); $i++ ) {
            $remainders[] = [ 'r' => $exact[ $i ] - $floored[ $i ], 'i' => $i ];
        }
        usort( $remainders, fn( $a, $b ) => $b['r'] <=> $a['r'] );

        $deficit = 100 - array_sum( $floored );
        foreach ( $remainders as $item ) {
            if ( $deficit <= 0 ) break;
            $floored[ $item['i'] ]++;
            $deficit--;
        }

        // Ensure no 0% column
        for ( $i = 0; $i < count( $floored ); $i++ ) {
            if ( $floored[ $i ] === 0 ) {
                $floored[ $i ] = 1;
                $max_idx = array_search( max( $floored ), $floored );
                $floored[ $max_idx ]--;
            }
        }

        return $floored;
    }

    // -----------------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------------

    public function get_visible_children( array $node ): array {
        $children = $node['children'] ?? [];
        return array_values( array_filter( $children, fn( $c ) => ( $c['visible'] ?? true ) !== false ) );
    }

    public function is_leaf_widget( array $node ): bool {
        $leaf_types = [ 'TEXT', 'VECTOR', 'LINE', 'STAR', 'POLYGON', 'ELLIPSE', 'BOOLEAN_OPERATION', 'RECTANGLE' ];
        if ( in_array( $node['type'] ?? '', $leaf_types, true ) ) return true;
        return empty( $this->get_visible_children( $node ) );
    }

    private function vertical_overlap( array $a, array $b ): float {
        $top_a    = $a['y'];
        $bottom_a = $a['y'] + $a['height'];
        $top_b    = $b['y'];
        $bottom_b = $b['y'] + $b['height'];
        return max( 0, min( $bottom_a, $bottom_b ) - max( $top_a, $top_b ) );
    }

    private function expand_bounds( array $a, array $b ): array {
        $x = min( $a['x'], $b['x'] );
        $y = min( $a['y'], $b['y'] );
        return [
            'x'      => $x,
            'y'      => $y,
            'width'  => max( $a['x'] + $a['width'], $b['x'] + $b['width'] ) - $x,
            'height' => max( $a['y'] + $a['height'], $b['y'] + $b['height'] ) - $y,
        ];
    }
}

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *


Notice: ob_end_flush(): Failed to send buffer of zlib output compression (1) in /home/greenspacecoil/public_html/wp-includes/functions.php on line 5493

Notice: ob_end_flush(): Failed to send buffer of zlib output compression (1) in /home/greenspacecoil/public_html/wp-includes/functions.php on line 5493