<?php
/**
 * Maps Figma leaf nodes to Elementor widget settings.
 */

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

class FTE_Widget_Mapper {

    /**
     * Convert a Figma leaf node into a logical widget array.
     *
     * @param array $node Figma node.
     * @return array [ 'type' => 'widget', 'widgetType' => ..., 'settings' => [...] ]
     */
    public function map( array $node ): array {
        $type = $node['type'] ?? '';

        switch ( $type ) {
            case 'TEXT':
                return $this->map_text( $node );
            case 'RECTANGLE':
                return $this->map_rectangle( $node );
            case 'ELLIPSE':
            case 'STAR':
            case 'POLYGON':
            case 'LINE':
            case 'VECTOR':
            case 'BOOLEAN_OPERATION':
                return $this->map_vector( $node );
            case 'FRAME':
            case 'GROUP':
            case 'COMPONENT':
            case 'INSTANCE':
                return $this->map_container( $node );
            default:
                return $this->map_fallback( $node );
        }
    }

    // -----------------------------------------------------------------------
    // Text
    // -----------------------------------------------------------------------

    private function map_text( array $node ): array {
        $style = $node['style'] ?? [];
        $text  = $node['characters'] ?? '';
        $size  = $style['fontSize'] ?? 14;

        $is_heading = $size >= 20 || strpos( $text, "\n" ) === false;

        if ( $is_heading && ! empty( $style ) ) {
            $settings = [
                'title'                      => $text,
                'header_size'                => $this->infer_header_size( $size ),
                'align'                      => $this->map_text_align( $style['textAlignHorizontal'] ?? '' ),
                'title_color'                => $this->extract_text_color( $node ),
                'typography_typography'       => 'custom',
                'typography_font_family'     => $style['fontFamily'] ?? 'Inter',
                'typography_font_size'       => [ 'size' => round( $size ), 'unit' => 'px' ],
                'typography_font_weight'     => (string) ( $style['fontWeight'] ?? 400 ),
            ];
            if ( ! empty( $style['lineHeightPx'] ) ) {
                $settings['typography_line_height'] = [ 'size' => round( $style['lineHeightPx'] ), 'unit' => 'px' ];
            }
            if ( ! empty( $style['letterSpacing'] ) ) {
                $settings['typography_letter_spacing'] = [ 'size' => round( $style['letterSpacing'] ), 'unit' => 'px' ];
            }

            return [ 'type' => 'widget', 'widgetType' => 'heading', 'settings' => $settings ];
        }

        // Rich text
        return [
            'type'       => 'widget',
            'widgetType' => 'text-editor',
            'settings'   => [ 'editor' => $this->wrap_in_html( $text, $node ) ],
        ];
    }

    private function infer_header_size( float $size ): string {
        if ( $size >= 48 ) return 'h1';
        if ( $size >= 36 ) return 'h2';
        if ( $size >= 28 ) return 'h3';
        if ( $size >= 22 ) return 'h4';
        if ( $size >= 18 ) return 'h5';
        return 'h6';
    }

    private function map_text_align( string $align ): string {
        return match( $align ) {
            'CENTER'    => 'center',
            'RIGHT'     => 'right',
            'JUSTIFIED' => 'justify',
            default     => 'left',
        };
    }

    private function extract_text_color( array $node ): ?string {
        foreach ( $node['fills'] ?? [] as $fill ) {
            if ( $fill['type'] === 'SOLID' && isset( $fill['color'] ) ) {
                return self::figma_color_to_hex( $fill['color'] );
            }
        }
        return null;
    }

    private function wrap_in_html( string $text, array $node ): string {
        $style = $node['style'] ?? [];
        $lines = explode( "\n", $text );
        $html  = implode( "<br>\n", array_map( fn( $l ) => $l ?: '<br>', $lines ) );

        $css = [];
        if ( ! empty( $style['fontFamily'] ) )   $css[] = 'font-family: ' . $style['fontFamily'];
        if ( ! empty( $style['fontSize'] ) )     $css[] = 'font-size: ' . $style['fontSize'] . 'px';
        if ( ! empty( $style['fontWeight'] ) )   $css[] = 'font-weight: ' . $style['fontWeight'];
        if ( ! empty( $style['lineHeightPx'] ) ) $css[] = 'line-height: ' . $style['lineHeightPx'] . 'px';

        $color = $this->extract_text_color( $node );
        if ( $color ) $css[] = 'color: ' . $color;

        $style_attr = ! empty( $css ) ? " style='" . esc_attr( implode( '; ', $css ) ) . "'" : '';
        return '<p' . $style_attr . '>' . $html . '</p>';
    }

    // -----------------------------------------------------------------------
    // Rectangle
    // -----------------------------------------------------------------------

    private function map_rectangle( array $node ): array {
        $box   = $node['absoluteBoundingBox'] ?? [];
        $fills = $node['fills'] ?? [];

        // Image fill → Image widget
        foreach ( $fills as $fill ) {
            if ( $fill['type'] === 'IMAGE' ) {
                $ref = $fill['imageRef'] ?? $node['id'];
                return [
                    'type'       => 'widget',
                    'widgetType' => 'image',
                    'settings'   => [
                        'image'      => [ 'url' => '__FIGMA_IMAGE_REF__:' . $ref, 'id' => '' ],
                        'image_size' => 'full',
                    ],
                ];
            }
        }

        // Thin horizontal rectangle → Divider
        $h = $box['height'] ?? 0;
        $w = $box['width'] ?? 0;
        if ( $h <= 5 && $w > $h * 10 ) {
            return [
                'type'       => 'widget',
                'widgetType' => 'divider',
                'settings'   => [
                    'style'  => 'solid',
                    'weight' => [ 'size' => round( $h ), 'unit' => 'px' ],
                    'color'  => $this->extract_fill_color( $fills ),
                ],
            ];
        }

        // Colored box → HTML widget; transparent → Spacer
        $bg = $this->extract_fill_color( $fills );
        if ( ! $bg || $bg === '#FFFFFF' ) {
            return [
                'type'       => 'widget',
                'widgetType' => 'spacer',
                'settings'   => [ 'space' => [ 'size' => round( $h ?: 20 ), 'unit' => 'px' ] ],
            ];
        }

        $radius = $node['cornerRadius'] ?? 0;
        return [
            'type'       => 'widget',
            'widgetType' => 'html',
            'settings'   => [
                'html' => sprintf(
                    "<div style='width:100%%;height:%dpx;background-color:%s;border-radius:%dpx;'></div>",
                    (int) round( min( $h ?: 100, 400 ) ), esc_attr( $bg ), round( $radius )
                ),
            ],
        ];
    }

    // -----------------------------------------------------------------------
    // Vector / Shape
    // -----------------------------------------------------------------------

    private function map_vector( array $node ): array {
        return [
            'type'       => 'widget',
            'widgetType' => 'image',
            'settings'   => [
                'image'                  => [ 'url' => '__FIGMA_IMAGE_REF__:' . $node['id'], 'id' => '' ],
                '_requires_image_export' => true,
            ],
        ];
    }

    // -----------------------------------------------------------------------
    // Container as widget (depth budget exhausted)
    // -----------------------------------------------------------------------

    /**
     * Detect a vector-group container (logo/icon): a FRAME/GROUP/COMPONENT/INSTANCE
     * whose direct visible children are mostly VECTOR/LINE/STAR/POLYGON/ELLIPSE/
     * BOOLEAN_OPERATION. Such groups should be exported as ONE image using the
     * container's own node id, not sliced into per-letter image widgets.
     */
    public function is_vector_group( array $node ): bool {
        $type = $node['type'] ?? '';
        if ( ! in_array( $type, [ 'FRAME', 'GROUP', 'COMPONENT', 'INSTANCE' ], true ) ) return false;
        $kids = array_values( array_filter( $node['children'] ?? [], fn( $c ) => ( $c['visible'] ?? true ) !== false ) );
        if ( count( $kids ) < 2 ) return false;
        $vector_types = [ 'VECTOR', 'LINE', 'STAR', 'POLYGON', 'ELLIPSE', 'BOOLEAN_OPERATION' ];
        $vec_count = 0;
        foreach ( $kids as $k ) {
            if ( in_array( $k['type'] ?? '', $vector_types, true ) ) $vec_count++;
        }
        return ( $vec_count / count( $kids ) ) >= 0.6;
    }

    private function emit_container_as_image( array $node ): array {
        return [
            'type'       => 'widget',
            'widgetType' => 'image',
            'settings'   => [
                'image'                  => [ 'url' => '__FIGMA_IMAGE_REF__:' . ( $node['id'] ?? '' ), 'id' => '' ],
                '_requires_image_export' => true,
                'image_size'             => 'full',
            ],
        ];
    }

    private function map_container( array $node ): array {
        if ( $this->is_vector_group( $node ) ) {
            return $this->emit_container_as_image( $node );
        }
        // Container frames are mostly layout, not leaves. Their background/padding are
        // captured by the converter via extract_row_settings / extract_column_settings,
        // so this method is only called when the converter has already decided this node
        // is a leaf (e.g., a decorative background frame with no useful children).
        //
        // Historical behavior emitted an HTML rectangle with the frame's literal height.
        // For tall outer frames (e.g., a 3730px hero wrapper) that inflated the parent
        // column by 3000+px and pushed sibling sections far below, producing the flat
        // stacked-sections-same-Y layout. We cap the emitted visual height so a
        // leaf-classified container can contribute color but never inflate geometry.
        $box    = $node['absoluteBoundingBox'] ?? [ 'height' => 20 ];
        $height = (float) ( $box['height'] ?? 20 );
        $bg     = $this->extract_fill_color( $node['fills'] ?? [] );
        $radius = (float) ( $node['cornerRadius'] ?? 0 );
        $capped = (int) round( min( $height, 80 ) );

        if ( $bg ) {
            return [
                'type'       => 'widget',
                'widgetType' => 'html',
                'settings'   => [
                    'html' => sprintf(
                        "<div style='width:100%%;height:%dpx;background:%s;border-radius:%dpx'></div>",
                        $capped, esc_attr( $bg ), (int) round( $radius )
                    ),
                ],
            ];
        }

        return [
            'type'       => 'widget',
            'widgetType' => 'spacer',
            'settings'   => [ 'space' => [ 'size' => (int) round( min( $height ?: 20, 20 ) ), 'unit' => 'px' ] ],
        ];
    }

    private function map_fallback( array $node ): array {
        return [
            'type'       => 'widget',
            'widgetType' => 'html',
            'settings'   => [ 'html' => '<!-- Unsupported: ' . esc_html( $node['type'] ?? 'unknown' ) . ' -->' ],
        ];
    }

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

    private function extract_fill_color( array $fills ): ?string {
        foreach ( $fills as $fill ) {
            if ( ( $fill['type'] ?? '' ) === 'SOLID' && isset( $fill['color'] ) ) {
                return self::figma_color_to_hex( $fill['color'] );
            }
        }
        return null;
    }

    public static function figma_color_to_hex( array $color ): string {
        $r   = (int) round( ( $color['r'] ?? 0 ) * 255 );
        $g   = (int) round( ( $color['g'] ?? 0 ) * 255 );
        $b   = (int) round( ( $color['b'] ?? 0 ) * 255 );
        $hex = sprintf( '#%02X%02X%02X', $r, $g, $b );

        $a = $color['a'] ?? 1;
        if ( $a < 1 ) {
            $hex .= sprintf( '%02X', (int) round( $a * 255 ) );
        }
        return $hex;
    }
}

כתיבת תגובה

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


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