Plugin Directory

Changeset 3612360


Ignore:
Timestamp:
07/18/2026 08:05:25 AM (3 months ago)
Author:
anydress
Message:

Release 1.3.1: size charts and true-to-size fit, async generation, try-on format settings, WP 7.0 compatibility

Location:
anydress-ai-virtual-try-on
Files:
10 added
31 edited
1 copied

Legend:

Unmodified
Added
Removed
  • anydress-ai-virtual-try-on/tags/1.3.1/anydress-ai-virtual-try-on.php

    r3530062 r3612360  
    44 * Plugin URI: https://anydress.ai/plugin/
    55 * Description: Elevate your digital boutique with seamless AI virtual try-on.
    6  * Version: 1.0.0
     6 * Version: 1.3.1
    77 * Author: AnyDress.ai
    88 * Author URI: https://anydress.ai
    … …  
    2020
    2121// Define plugin constants for easy reference later
    22 define( 'ANYDRESS_VERSION', '1.0.0' ); // Added version for cache-busting CSS/JS
     22define( 'ANYDRESS_VERSION', '1.3.1' ); // Added version for cache-busting CSS/JS
    2323define( 'ANYDRESS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2424define( 'ANYDRESS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    … …  
    5050    }
    5151   
     52    // 0. Load the shared Size Config (needed by Admin UI, AJAX, and Frontend alike)
     53    require_once ANYDRESS_PLUGIN_DIR . 'includes/anydress-size-config.php';
     54
    5255    // 1. Load Admin UI (Only in WP Admin, but NOT during AJAX)
    5356    if ( is_admin() && ! wp_doing_ajax() ) {
    5457        require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-settings.php';
    5558        require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-photoshoot.php';
     59        require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-size-charts.php';
     60        require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-product-sizes.php';
    5661    }
    5762
    5863    // 2. Load AJAX Handlers (Only when an AJAX request is firing)
    5964    if ( wp_doing_ajax() ) {
     65        // Async task status relay (shared by frontend + admin)
     66        require_once ANYDRESS_PLUGIN_DIR . 'includes/anydress-task-client.php';
    6067        // Admin AJAX
    6168        require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/ajax/ajax-generate-photoshoot.php';
  • anydress-ai-virtual-try-on/tags/1.3.1/assets/css/frontend-tryon.css

    r3530062 r3612360  
    127127    border-color: #222; /* Default border */
    128128}
     129/* ==========================================================================
     130   Sizing & Fit section (optional accurate-fit inputs)
     131   ========================================================================== */
     132.anydress-fit-section {
     133    margin: 18px 0 5px;
     134    text-align: left;
     135}
     136
     137.anydress-fit-section details {
     138    border: 1px solid #e2e2e2;
     139    border-radius: 8px;
     140    background: rgba(255, 255, 255, 0.6);
     141}
     142
     143.anydress-fit-section summary {
     144    padding: 12px 15px;
     145    cursor: pointer;
     146    font-weight: 600;
     147    font-size: 14px;
     148    user-select: none;
     149}
     150
     151.anydress-fit-body {
     152    padding: 5px 15px 15px;
     153    border-top: 1px solid #eee;
     154}
     155
     156.anydress-fit-row {
     157    display: flex;
     158    align-items: center;
     159    gap: 12px;
     160    margin: 10px 0;
     161    font-size: 13px;
     162}
     163
     164.anydress-fit-row > label {
     165    min-width: 140px;
     166    font-weight: 600;
     167}
     168
     169.anydress-fit-row select {
     170    padding: 6px 10px;
     171    border: 1px solid #ccc;
     172    border-radius: 6px;
     173    max-width: 180px;
     174}
     175
     176.anydress-fit-unit-opt {
     177    margin-right: 12px;
     178    cursor: pointer;
     179}
     180
     181.anydress-fit-hint {
     182    font-size: 12px;
     183    color: #777;
     184    margin: 8px 0 10px;
     185    line-height: 1.4;
     186}
     187
     188.anydress-fit-fields {
     189    display: grid;
     190    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
     191    gap: 10px;
     192}
     193
     194.anydress-fit-field label {
     195    display: block;
     196    font-size: 12px;
     197    font-weight: 600;
     198    margin-bottom: 3px;
     199    line-height: 1.3;
     200}
     201
     202.anydress-fit-field small {
     203    font-weight: normal;
     204    color: #888;
     205}
     206
     207.anydress-fit-field input {
     208    width: 100%;
     209    padding: 6px 8px;
     210    border: 1px solid #ccc;
     211    border-radius: 6px;
     212    font-size: 13px;
     213    box-sizing: border-box;
     214}
     215
     216.anydress-fit-warning {
     217    background: #fff8e5;
     218    border-left: 4px solid #dba617;
     219    padding: 10px 14px;
     220    margin: 12px 0;
     221    font-size: 13px;
     222    text-align: left;
     223    border-radius: 0 6px 6px 0;
     224    line-height: 1.4;
     225}
  • anydress-ai-virtual-try-on/tags/1.3.1/assets/js/admin-photoshoot.js

    r3530062 r3612360  
    1515        });
    1616    }
     17    // ======================================================================
     18    // ASYNC TASK RESOLUTION
     19    // The server accepts jobs in seconds and returns a task_id; we poll a
     20    // lightweight status action until the image is ready (or the task fails).
     21    // ======================================================================
     22    async function anydressResolveTask(data) {
     23        if (!data || !data.success || !data.data || !data.data.task_id) {
     24            return data; // not a task response — pass through unchanged
     25        }
     26        const taskId = data.data.task_id;
     27
     28        for (let i = 0; i < 130; i++) { // ~9 minutes max (server times tasks out at 8)
     29            await new Promise(r => setTimeout(r, i === 0 ? 6000 : 4000));
     30            try {
     31                const fd = new FormData();
     32                fd.append('action', 'anydress_admin_check_task');
     33                fd.append('task_id', taskId);
     34                fd.append('nonce', document.getElementById('anydress_nonce').value);
     35
     36                const resp = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: fd });
     37                const status = await resp.json();
     38
     39                if (status && status.success && status.data && status.data.status === 'pending') {
     40                    continue; // still generating
     41                }
     42                return status; // completed (has image_url) or failed (error shape)
     43            } catch (e) {
     44                // transient network hiccup — keep polling
     45            }
     46        }
     47        return { success: false, data: { message: 'Generation timed out. Please try again.' } };
     48    }
     49
    1750    let originalImageUrl = "";
    1851    let currentIsGhost = false;
    … …  
    436469                let response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData });
    437470                let data = await response.json();
     471                data = await anydressResolveTask(data);
    438472
    439473                // Fallback Interceptor
    … …  
    441475                    const loadingText = loadingBox.querySelector('p');
    442476                    if(loadingText) loadingText.innerText = "Optimizing... Please wait another 20-70 seconds";
    443                    
     477
    444478                    formData.set('force_fallback', 'true');
    445479                    response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData });
    446480                    data = await response.json();
     481                    data = await anydressResolveTask(data);
    447482                }
    448483
    … …  
    662697            let response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData });
    663698            let data = await response.json();
     699            data = await anydressResolveTask(data);
    664700
    665701            // Fallback Interceptor
    666702            if (!data.success && data.data && data.data.error_code === 'require_fallback') {
    667703                loadingText.innerText = "Optimizing... Please wait another 20-70 seconds";
    668                
     704
    669705                formData.set('force_fallback', 'true');
    670706                response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData });
    671707                data = await response.json();
     708                data = await anydressResolveTask(data);
    672709            }
    673710
    … …  
    762799            let response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData });
    763800            let data = await response.json();
     801            data = await anydressResolveTask(data);
    764802
    765803            // Fallback Interceptor
    766804            if (!data.success && data.data && data.data.error_code === 'require_fallback') {
    767805                loadingText.innerText = "Optimizing... Please wait another 20-70 seconds";
    768                
     806
    769807                formData.set('force_fallback', 'true');
    770808                response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData });
    771809                data = await response.json();
     810                data = await anydressResolveTask(data);
    772811            }
    773812
  • anydress-ai-virtual-try-on/tags/1.3.1/assets/js/frontend-tryon.js

    r3530062 r3612360  
    7575    }
    7676
     77    // ======================================================================
     78    // SIZING & FIT (optional accurate-fit section)
     79    // ======================================================================
     80    var sizing = anydressFrontend.sizing || null;
     81    var fitSizeSelect = document.getElementById("anydress-fit-size");
     82
     83    function fitActiveUnit() {
     84        var checked = document.querySelector('input[name="anydress_fit_unit"]:checked');
     85        return checked ? checked.value : "cm";
     86    }
     87
     88    function fitUpdateUnitLabels() {
     89        var unit = fitActiveUnit();
     90        document.querySelectorAll(".anydress-fit-unit-label").forEach(function (el) {
     91            el.textContent = "(" + unit + ")";
     92        });
     93    }
     94
     95    function fitInitInputs() {
     96        if (!sizing) { return; }
     97        var unit = fitActiveUnit();
     98        var saved = sizing.saved || {};
     99        document.querySelectorAll(".anydress-fit-input").forEach(function (input) {
     100            var key = input.getAttribute("data-key");
     101            var metric = saved[key]; // stored in cm (body) or mm (face)
     102            if (typeof metric === "undefined" || metric === null || metric === "") { return; }
     103            metric = parseFloat(metric);
     104            if (!(metric > 0)) { return; }
     105            if (input.getAttribute("data-fixed-unit") === "mm") {
     106                input.value = metric;
     107            } else {
     108                input.value = (unit === "in") ? Math.round((metric / 2.54) * 10) / 10 : metric;
     109            }
     110        });
     111        fitUpdateUnitLabels();
     112    }
     113
     114    // Unit toggle: convert already-entered values between cm and in
     115    document.querySelectorAll('input[name="anydress_fit_unit"]').forEach(function (radio) {
     116        radio.addEventListener("change", function () {
     117            var toUnit = radio.value;
     118            document.querySelectorAll(".anydress-fit-input").forEach(function (input) {
     119                if (input.getAttribute("data-fixed-unit") === "mm") { return; }
     120                var v = parseFloat(input.value);
     121                if (!(v > 0)) { return; }
     122                input.value = (toUnit === "in")
     123                    ? Math.round((v / 2.54) * 10) / 10
     124                    : Math.round(v * 2.54 * 10) / 10;
     125            });
     126            fitUpdateUnitLabels();
     127        });
     128    });
     129
     130    // Sync the size picker with the WooCommerce variation dropdown (if one exists)
     131    function fitSyncFromVariation() {
     132        if (!sizing || !sizing.sizeAttrField || !fitSizeSelect) { return; }
     133        var variationSelect = document.querySelector('select[name="' + sizing.sizeAttrField + '"]');
     134        if (!variationSelect || !variationSelect.value) { return; }
     135        var chosen = variationSelect.selectedOptions.length
     136            ? variationSelect.selectedOptions[0].textContent.trim()
     137            : variationSelect.value;
     138        var candidates = [variationSelect.value.toLowerCase(), chosen.toLowerCase()];
     139        for (var i = 0; i < fitSizeSelect.options.length; i++) {
     140            var optVal = fitSizeSelect.options[i].value.toLowerCase().trim();
     141            if (optVal !== "" && candidates.indexOf(optVal) !== -1) {
     142                fitSizeSelect.value = fitSizeSelect.options[i].value;
     143                return;
     144            }
     145        }
     146    }
     147
     148    if (sizing && sizing.sizeAttrField) {
     149        var wooSizeSelect = document.querySelector('select[name="' + sizing.sizeAttrField + '"]');
     150        if (wooSizeSelect) {
     151            wooSizeSelect.addEventListener("change", fitSyncFromVariation);
     152        }
     153    }
     154
     155    if (sizing) {
     156        fitInitInputs();
     157        fitSyncFromVariation();
     158    }
     159
     160    // Append the shopper's sizing choices to an outgoing request
     161    function fitAppendToForm(formData) {
     162        if (!sizing) { return; }
     163
     164        if (fitSizeSelect && fitSizeSelect.value) {
     165            formData.append("size_label", fitSizeSelect.value);
     166        }
     167        formData.append("measure_unit", fitActiveUnit());
     168
     169        var any = false;
     170        document.querySelectorAll(".anydress-fit-input").forEach(function (input) {
     171            var v = parseFloat(input.value);
     172            if (v > 0) {
     173                formData.append("measurements[" + input.getAttribute("data-key") + "]", v);
     174                any = true;
     175            }
     176        });
     177
     178        if (any || (fitSizeSelect && fitSizeSelect.value)) {
     179            formData.append("sizing_active", "true");
     180        }
     181    }
     182
     183    // ======================================================================
     184    // ASYNC TASK RESOLUTION
     185    // The server now accepts the job in seconds and returns a task_id; we poll
     186    // a lightweight status action until the image is ready (or the task fails).
     187    // ======================================================================
     188    async function anydressResolveTask(data) {
     189        if (!data || !data.success || !data.data || !data.data.task_id) {
     190            return data; // not a task response — pass through unchanged
     191        }
     192        var taskId = data.data.task_id;
     193
     194        for (var i = 0; i < 130; i++) { // ~9 minutes max (server times tasks out at 8)
     195            await new Promise(function (r) { setTimeout(r, i === 0 ? 6000 : 4000); });
     196            try {
     197                var fd = new FormData();
     198                fd.append("action", "anydress_check_task");
     199                fd.append("task_id", taskId);
     200                fd.append("nonce", anydressFrontend.nonce);
     201
     202                var resp = await fetch(anydressFrontend.ajaxUrl, { method: "POST", body: fd });
     203                var status = await resp.json();
     204
     205                if (status && status.success && status.data && status.data.status === "pending") {
     206                    continue; // still generating
     207                }
     208                return status; // completed (has image_url) or failed (error shape)
     209            } catch (e) {
     210                // transient network hiccup — keep polling
     211            }
     212        }
     213        return { success: false, data: { message: "Generation timed out. Please try again." } };
     214    }
     215
    77216    // Central function to send AJAX request
    78217    async function sendToEngine(formData, buttonToDisable) {
    … …  
    80219            formData.append("force_fallback", "false");
    81220        }
     221        fitAppendToForm(formData);
    82222
    83223        buttonToDisable.innerText = "Processing... (This takes 20-70 seconds)";
    … …  
    95235            let response = await fetch(anydressFrontend.ajaxUrl, { method: "POST", body: formData });
    96236            let data = await response.json();
     237            data = await anydressResolveTask(data);
    97238
    98239            if (!data.success && data.data && data.data.error_code === 'require_fallback') {
    99240                resultArea.innerHTML = "<p><i>Optimizing... Please wait another 20-70 seconds</i></p>";
    100241                buttonToDisable.innerText = "Optimizing...";
    101                
     242
    102243                formData.set("force_fallback", "true");
    103244                response = await fetch(anydressFrontend.ajaxUrl, { method: "POST", body: formData });
    104245                data = await response.json();
     246                data = await anydressResolveTask(data);
    105247            }
    106248
    … …  
    128270                }
    129271
     272                // Optional fit warning coming back from the sizing engine
     273                var fitWarningHtml = "";
     274                if (data.data.fit_warning) {
     275                    var warnText = document.createElement("div");
     276                    warnText.textContent = data.data.fit_warning; // XSS-safe
     277                    fitWarningHtml = "<div class='anydress-fit-warning'>⚠️ <b>" + (anydressFrontend.i18n.fitWarning || "Fit note:") + "</b> " + warnText.innerHTML + "</div>";
     278                }
     279
    130280                // Display the image
    131                 resultArea.innerHTML = "<h3>Your Virtual Try-On:</h3><img src='" + data.data.image_url + "' style='max-width:100%; max-height:60vh; object-fit:contain; display:block; margin: 15px auto 0; border-radius:8px; box-shadow: 0 4px 10px rgba(0,0,0,0.1);' />";
     281                resultArea.innerHTML = "<h3>Your Virtual Try-On:</h3>" + fitWarningHtml + "<img src='" + data.data.image_url + "' style='max-width:100%; max-height:60vh; object-fit:contain; display:block; margin: 15px auto 0; border-radius:8px; box-shadow: 0 4px 10px rgba(0,0,0,0.1);' />";
    132282            } else {
    133283                // Error: Re-enable button and SHOW the link again
    134                 buttonToDisable.innerText = "Try Again";
    135284                buttonToDisable.disabled = false;
    136285                if (showUploadLink) showUploadLink.style.display = "";
    137                
    138                 // Handle object vs string error messages cleanly
     286
     287                // Handle object vs string error messages cleanly (XSS-safe)
    139288                let errorMsg = typeof data.data === 'string' ? data.data : (data.data.message || 'Unknown error');
    140                 resultArea.innerHTML = "<p style='color:red; font-weight:bold;'>Error: " + errorMsg + "</p>";
     289                var safeMsg = document.createElement("div");
     290                safeMsg.textContent = errorMsg;
     291
     292                // Fit-advice blocks are helpful sizing guidance, not real errors —
     293                // render them as a friendly amber notice instead of red error text.
     294                var fitCodes = ["size_too_small", "size_too_large", "size_length_issue",
     295                    "frame_too_narrow", "frame_too_wide", "bridge_too_narrow", "bridge_too_wide",
     296                    "arms_too_short", "arms_too_long", "headwear_too_small", "headwear_too_large"];
     297                var errCode = (data.data && data.data.error_code) ? data.data.error_code : "";
     298
     299                if (fitCodes.indexOf(errCode) !== -1) {
     300                    buttonToDisable.innerText = "Try Another Size";
     301                    resultArea.innerHTML = "<div class='anydress-fit-warning'>📏 <b>Sizing advice:</b> " + safeMsg.innerHTML + "</div>";
     302                    // Draw the shopper's eye to the size picker so they can switch and retry
     303                    var fitDetails = document.getElementById("anydress-fit-details");
     304                    if (fitDetails) { fitDetails.open = true; }
     305                } else {
     306                    buttonToDisable.innerText = "Try Again";
     307                    resultArea.innerHTML = "<p style='color:red; font-weight:bold;'>Error: " + safeMsg.innerHTML + "</p>";
     308                }
    141309            }
    142310
    … …  
    201369                formData.append("use_saved_mannequin", "false");
    202370                formData.append("face_image", compressedFace);
    203                 formData.append("face_image", compressedFace);
    204371                formData.append("consent_given", "true");
    205372
  • anydress-ai-virtual-try-on/tags/1.3.1/includes/admin/ajax/ajax-generate-edit.php

    r3530062 r3612360  
    7575        'headers' => array( 'Content-Type' => 'application/json' ),
    7676        'body'    => wp_json_encode( $payload ),
    77         'timeout' => 120
     77        'timeout' => 60
    7878    ));
    7979
    … …  
    9191            wp_send_json_error( array( 'error_code' => 'require_fallback', 'message' => $data['message'] ) );
    9292        }
    93         if ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) {
     93        if ( ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) || ( isset( $data['data']['code'] ) && $data['data']['code'] === 'insufficient_credits' ) ) {
    9494            wp_send_json_error( array( 'error_code' => 'insufficient_credits', 'message' => $data['message'] ) );
    9595        }
    … …  
    9797    }
    9898
    99     if ( empty( $data['image_url'] ) ) wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
     99    // ASYNC: the server returns a task_id; the browser polls 'anydress_admin_check_task'
     100    if ( empty( $data['task_id'] ) ) wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
    100101
    101     wp_send_json_success( array( 'image_url' => $data['image_url'] ) );
     102    wp_send_json_success( array( 'task_id' => sanitize_text_field( $data['task_id'] ), 'status' => 'pending' ) );
    102103}
  • anydress-ai-virtual-try-on/tags/1.3.1/includes/admin/ajax/ajax-generate-photoshoot.php

    r3530062 r3612360  
    189189        'headers' => array( 'Content-Type' => 'application/json' ),
    190190        'body'    => wp_json_encode( $payload ),
    191         'timeout' => 120
     191        'timeout' => 60
    192192    ));
    193193
    … …  
    206206        }
    207207        // NEW: Gracefully catch the out of credits error so the frontend can display an upgrade prompt
    208         if ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) {
     208        if ( ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) || ( isset( $data['data']['code'] ) && $data['data']['code'] === 'insufficient_credits' ) ) {
    209209            wp_send_json_error( array( 'error_code' => 'insufficient_credits', 'message' => $data['message'] ) );
    210210        }
    … …  
    212212    }
    213213
    214     if ( empty( $data['image_url'] ) ) wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
    215 
    216     wp_send_json_success( array( 'image_url' => $data['image_url'] ) );
     214    // ASYNC: the server returns a task_id; the browser polls 'anydress_admin_check_task'
     215    if ( empty( $data['task_id'] ) ) wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
     216
     217    wp_send_json_success( array( 'task_id' => sanitize_text_field( $data['task_id'] ), 'status' => 'pending' ) );
    217218}
    218219
  • anydress-ai-virtual-try-on/tags/1.3.1/includes/admin/ajax/ajax-magic-edit.php

    r3530062 r3612360  
    7575        'headers' => array( 'Content-Type' => 'application/json' ),
    7676        'body'    => wp_json_encode( $payload ),
    77         'timeout' => 120
     77        'timeout' => 60
    7878    ));
    7979
    … …  
    9191            wp_send_json_error( array( 'error_code' => 'require_fallback', 'message' => $data['message'] ) );
    9292        }
    93         if ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) {
     93        if ( ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) || ( isset( $data['data']['code'] ) && $data['data']['code'] === 'insufficient_credits' ) ) {
    9494            wp_send_json_error( array( 'error_code' => 'insufficient_credits', 'message' => $data['message'] ) );
    9595        }
    … …  
    9797    }
    9898
    99     if ( empty( $data['image_url'] ) ) wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
     99    // ASYNC: the server returns a task_id; the browser polls 'anydress_admin_check_task'
     100    if ( empty( $data['task_id'] ) ) wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
    100101
    101     wp_send_json_success( array( 'image_url' => $data['image_url'] ) );
     102    wp_send_json_success( array( 'task_id' => sanitize_text_field( $data['task_id'] ), 'status' => 'pending' ) );
    102103}
  • anydress-ai-virtual-try-on/tags/1.3.1/includes/admin/class-anydress-photoshoot.php

    r3530062 r3612360  
    242242                                <option value="4K"><?php esc_html_e( '4K (Ultra High Definition)', 'anydress-ai-virtual-try-on' ); ?></option>
    243243                            </select>
     244                            <p style="font-size: 11px; color: #888; margin: 6px 0 0;"><?php esc_html_e( 'Note: swimwear, lingerie and similar items are processed by a dedicated engine that tops out at 2K — a 4K request for those items is generated and billed at 2K automatically.', 'anydress-ai-virtual-try-on' ); ?></p>
    244245                        </div>
    245246
  • anydress-ai-virtual-try-on/tags/1.3.1/includes/admin/class-anydress-settings.php

    r3530062 r3612360  
    279279        );
    280280
     281        register_setting( 'anydress_settings_group', 'anydress_fit_mode', array( $this, 'sanitize_fit_mode' ) );
     282
     283        add_settings_field(
     284            'anydress_fit_mode_field',
     285            __( 'Sizing: Bad Fit Behavior', 'anydress-ai-virtual-try-on' ),
     286            array( $this, 'fit_mode_render' ),
     287            'anydress-settings',
     288            'anydress_settings_section'
     289        );
     290
     291        register_setting( 'anydress_settings_group', 'anydress_vto_aspect', array( $this, 'sanitize_vto_aspect' ) );
     292        register_setting( 'anydress_settings_group', 'anydress_vto_resolution', array( $this, 'sanitize_vto_resolution' ) );
     293
     294        add_settings_field(
     295            'anydress_vto_aspect_field',
     296            __( 'Try-On Image Aspect Ratio', 'anydress-ai-virtual-try-on' ),
     297            array( $this, 'vto_aspect_render' ),
     298            'anydress-settings',
     299            'anydress_settings_section'
     300        );
     301
     302        add_settings_field(
     303            'anydress_vto_resolution_field',
     304            __( 'Try-On Image Resolution', 'anydress-ai-virtual-try-on' ),
     305            array( $this, 'vto_resolution_render' ),
     306            'anydress-settings',
     307            'anydress_settings_section'
     308        );
     309
    281310        // ==========================================
    282311        // 2. APPEARANCE SETTINGS
    … …  
    375404    }
    376405   
     406    public function sanitize_fit_mode( $value ) {
     407        return in_array( $value, array( 'block', 'warn' ), true ) ? $value : 'block';
     408    }
     409
     410    public function sanitize_vto_aspect( $value ) {
     411        return in_array( $value, array( '2:3', '3:4', '4:5' ), true ) ? $value : '3:4';
     412    }
     413
     414    public function sanitize_vto_resolution( $value ) {
     415        return in_array( $value, array( '1K', '2K' ), true ) ? $value : '1K';
     416    }
     417
     418    public function vto_aspect_render() {
     419        $val = get_option( 'anydress_vto_aspect', '3:4' );
     420        $options = array(
     421            '2:3' => __( '2:3 — Classic fashion editorial (tallest)', 'anydress-ai-virtual-try-on' ),
     422            '3:4' => __( '3:4 — Balanced portrait (default)', 'anydress-ai-virtual-try-on' ),
     423            '4:5' => __( '4:5 — Ecommerce / Instagram standard', 'anydress-ai-virtual-try-on' ),
     424        );
     425        echo '<select name="anydress_vto_aspect">';
     426        foreach ( $options as $key => $label ) {
     427            echo '<option value="' . esc_attr( $key ) . '" ' . selected( $val, $key, false ) . '>' . esc_html( $label ) . '</option>';
     428        }
     429        echo '</select>';
     430        echo '<p class="description">' . esc_html__( 'Every shopper try-on image is generated in this fixed portrait format, so results always look consistent across your store.', 'anydress-ai-virtual-try-on' ) . '</p>';
     431    }
     432
     433    public function vto_resolution_render() {
     434        $val = get_option( 'anydress_vto_resolution', '1K' );
     435        echo '<label style="margin-right: 15px;"><input type="radio" name="anydress_vto_resolution" value="1K" ' . checked( $val, '1K', false ) . ' /> ' . esc_html__( '1K — Standard (1 credit rate)', 'anydress-ai-virtual-try-on' ) . '</label>';
     436        echo '<label><input type="radio" name="anydress_vto_resolution" value="2K" ' . checked( $val, '2K', false ) . ' /> ' . esc_html__( '2K — High detail (1.5× credit rate)', 'anydress-ai-virtual-try-on' ) . '</label>';
     437        echo '<p class="description">' . esc_html__( 'Resolution of shopper try-on images. 2K looks sharper on zoom but each try-on costs 1.5× the credits and loads slightly slower on mobile.', 'anydress-ai-virtual-try-on' ) . '</p>';
     438    }
     439
     440    public function fit_mode_render() {
     441        $val = get_option( 'anydress_fit_mode', 'block' );
     442
     443        echo '<label style="display: block; margin-bottom: 8px;">';
     444        echo '<input type="radio" name="anydress_fit_mode" value="block" ' . checked( 'block', $val, false ) . ' /> ';
     445        echo '<strong>' . esc_html__( 'Block generation', 'anydress-ai-virtual-try-on' ) . '</strong> — ' . esc_html__( 'if the shopper\'s measurements say the selected size won\'t fit, skip the image and suggest a better size. Saves credits.', 'anydress-ai-virtual-try-on' );
     446        echo '</label>';
     447
     448        echo '<label style="display: block;">';
     449        echo '<input type="radio" name="anydress_fit_mode" value="warn" ' . checked( 'warn', $val, false ) . ' /> ';
     450        echo '<strong>' . esc_html__( 'Warn but generate', 'anydress-ai-virtual-try-on' ) . '</strong> — ' . esc_html__( 'show a fit warning but still generate the image with a realistically tight or loose fit. Uses credits even for bad fits.', 'anydress-ai-virtual-try-on' );
     451        echo '</label>';
     452
     453        echo '<p class="description">' . wp_kses_post( __( 'Only applies when a product has a <strong>Size Chart</strong> assigned and the shopper provides their body measurements. Manage charts under <strong>AnyDress AI → Size Charts</strong>.', 'anydress-ai-virtual-try-on' ) ) . '</p>';
     454    }
     455
    377456    public function privacy_policy_url_render() {
    378457        $val = get_option( 'anydress_privacy_policy_url', '' );
  • anydress-ai-virtual-try-on/tags/1.3.1/includes/frontend/ajax/ajax-frontend-tryon.php

    r3530062 r3612360  
    244244
    245245    // ====================================================================
     246    // SIZING & FIT: Build size_data from the store's chart + shopper input
     247    // ====================================================================
     248    $size_data = null;
     249
     250    $sizing_active = isset( $_POST['sizing_active'] ) && $_POST['sizing_active'] === 'true';
     251
     252    if ( $sizing_active && function_exists( 'anydress_size_resolve_garment_data' ) ) {
     253
     254        $size_label = isset( $_POST['size_label'] ) ? sanitize_text_field( wp_unslash( $_POST['size_label'] ) ) : '';
     255        $unit       = ( isset( $_POST['measure_unit'] ) && sanitize_text_field( wp_unslash( $_POST['measure_unit'] ) ) === 'in' ) ? 'in' : 'cm';
     256
     257        // 1. Garment measurements: resolved server-side from the product's size chart.
     258        //    The browser only ever sends the size LABEL — measurements are trusted store data.
     259        $size_data = anydress_size_resolve_garment_data( $product_id, $size_label );
     260
     261        if ( $size_data ) {
     262            // 2. Shopper measurements (optional). Sanitized against the product type's
     263            //    known field list and converted to metric.
     264            $raw_measurements = array();
     265            if ( isset( $_POST['measurements'] ) && is_array( $_POST['measurements'] ) ) {
     266                // Values are numeric strings; numeric validation happens field-by-field
     267                // in anydress_size_sanitize_shopper_measurements() right after.
     268                $raw_measurements = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['measurements'] ) );
     269            }
     270
     271            $shopper = anydress_size_sanitize_shopper_measurements( $raw_measurements, $size_data['product_type'], $unit );
     272
     273            if ( ! empty( $shopper ) ) {
     274                // Persist for reuse (same lifecycle as the mannequin: My Account deletion,
     275                // 7-day cron, uninstall). Merge so switching product types keeps other fields.
     276                $existing = get_user_meta( $user_id, 'anydress_shopper_measurements', true );
     277                if ( ! is_array( $existing ) ) {
     278                    $existing = array();
     279                }
     280                update_user_meta( $user_id, 'anydress_shopper_measurements', array_merge( $existing, $shopper ) );
     281                update_user_meta( $user_id, 'anydress_shopper_unit', $unit );
     282
     283                $size_data = array_merge( $size_data, $shopper );
     284                $size_data['fit_mode'] = anydress_size_get_fit_mode();
     285            } else {
     286                // Size chosen but no body measurements: fit validation is impossible.
     287                // Send nothing so the try-on behaves exactly like the standard flow.
     288                $size_data = null;
     289            }
     290        }
     291    }
     292
     293    // ====================================================================
    246294    // SAAS ROUTING: Send to AnyDress.ai Server
    247295    // ====================================================================
    … …  
    255303
    256304    $payload = array(
    257         'license_key'    => $api_key, 
     305        'license_key'    => $api_key,
    258306        'client_domain'  => $safe_domain,
    259         'user_hash'      => $user_hash, 
     307        'user_hash'      => $user_hash,
    260308        'force_fallback' => $force_fallback,
     309        'aspect_ratio'   => get_option( 'anydress_vto_aspect', '3:4' ),
     310        'resolution'     => get_option( 'anydress_vto_resolution', '1K' ),
    261311        'face_base64'    => $face_base64,
    262312        'face_mime'      => $face_mime,
    … …  
    267317    );
    268318
     319    // Attach sizing payload only when fully resolved (garment + shopper data)
     320    if ( ! empty( $size_data ) ) {
     321        $payload['size_data'] = $size_data;
     322    }
     323
    269324    $response = wp_remote_post( $saas_endpoint, array(
    270325        'headers' => array( 'Content-Type' => 'application/json' ),
    271326        'body'    => wp_json_encode( $payload ),
    272         'timeout' => 120 // 120 seconds to allow for image generation
     327        'timeout' => 60 // Submission returns a task_id in seconds; polling handles the rest
    273328    ));
    274329
    … …  
    284339    // Handle errors returned by the SaaS Server
    285340    if ( $status_code !== 200 ) {
    286         // Propagate the specific fallback error code to frontend shopper
    287         if ( isset( $data['code'] ) && $data['code'] === 'require_fallback' ) {
    288             wp_send_json_error( array( 'error_code' => 'require_fallback', 'message' => $data['message'] ) );
    289         }
    290         $error_msg = isset( $data['message'] ) ? $data['message'] : __( 'Unknown Server Error.', 'anydress-ai-virtual-try-on' );
    291         wp_send_json_error( $error_msg );
    292     }
    293 
    294     if ( empty( $data['image_url'] ) ) {
    295         wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
    296     }
     341        // Always relay the server's error code so the JS can style fit-advice
     342        // blocks (size_too_small etc.) differently from real errors.
     343        $error_code = isset( $data['code'] ) ? sanitize_key( $data['code'] ) : 'server_error';
     344        $error_msg  = isset( $data['message'] ) ? $data['message'] : __( 'Unknown Server Error.', 'anydress-ai-virtual-try-on' );
     345        wp_send_json_error( array( 'error_code' => $error_code, 'message' => $error_msg ) );
     346    }
     347
     348    // ASYNC: the server now returns a task_id immediately. The browser polls
     349    // the 'anydress_check_task' action until the image is ready.
     350    if ( empty( $data['task_id'] ) ) {
     351        wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
     352    }
     353
     354    // Count the daily-limit slot NOW (prevents bypassing the limit with parallel
     355    // submissions). If the task terminally fails, the status relay refunds it.
    297356    update_user_meta( $user_id, 'anydress_usage_count', $usage_count + 1 );
    298357
    299     // Success! Send it to the frontend.
    300     wp_send_json_success( array( 'image_url' => $data['image_url'] ) );
     358    wp_send_json_success( array(
     359        'task_id' => sanitize_text_field( $data['task_id'] ),
     360        'status'  => 'pending',
     361    ) );
    301362}
  • anydress-ai-virtual-try-on/tags/1.3.1/includes/frontend/anydress-cron.php

    r3530062 r3612360  
    5656            // 4. Remove timestamp
    5757            delete_user_meta( $user_id, 'anydress_mannequin_upload_time' );
     58
     59            // 5. Delete saved body measurements (same privacy lifecycle as photos)
     60            delete_user_meta( $user_id, 'anydress_shopper_measurements' );
     61            delete_user_meta( $user_id, 'anydress_shopper_unit' );
    5862        }
    5963    }
  • anydress-ai-virtual-try-on/tags/1.3.1/includes/frontend/class-anydress-my-account.php

    r3530062 r3612360  
    1616        add_filter( 'woocommerce_account_menu_items', array( $this, 'add_my_account_menu_item' ) );
    1717        add_action( 'template_redirect', array( $this, 'handle_mannequin_deletion' ) );
     18        add_action( 'template_redirect', array( $this, 'handle_measurements_save' ) );
    1819        add_action( 'woocommerce_account_digital-mannequin_endpoint', array( $this, 'my_account_endpoint_content' ) );
     20    }
     21
     22    /**
     23     * All body-measurement fields a shopper can store, across every product type.
     24     * Returns array of payload_key => array( label, unit ).
     25     */
     26    private function get_all_shopper_fields() {
     27        $fields = array();
     28        if ( ! function_exists( 'anydress_size_get_product_types' ) ) {
     29            return $fields;
     30        }
     31        foreach ( anydress_size_get_product_types() as $type ) {
     32            foreach ( $type['shopper_fields'] as $key => $label ) {
     33                if ( ! isset( $fields[ $key ] ) ) {
     34                    $fields[ $key ] = array( 'label' => $label, 'unit' => $type['unit'] );
     35                }
     36            }
     37        }
     38        return $fields;
    1939    }
    2040
    … …  
    84104           
    85105            // 4. Remove timestamp
    86             delete_user_meta( $user_id, 'anydress_mannequin_upload_time' );
     106            delete_user_meta( $user_id, 'anydress_mannequin_upload_time' );
     107
     108            // 5. Delete saved body measurements
     109            delete_user_meta( $user_id, 'anydress_shopper_measurements' );
     110            delete_user_meta( $user_id, 'anydress_shopper_unit' );
    87111
    88112            // Show a success message in WooCommerce
    … …  
    90114           
    91115            // Redirect back to the tab to refresh the page safely
     116            wp_safe_redirect( wc_get_endpoint_url( 'digital-mannequin', '', wc_get_page_permalink( 'myaccount' ) ) );
     117            exit;
     118        }
     119    }
     120
     121    /**
     122     * Handle saving of body measurements from the My Account tab
     123     */
     124    public function handle_measurements_save() {
     125        if ( is_account_page() && isset( $_POST['anydress_save_measurements'] ) && is_user_logged_in() ) {
     126            if ( ! isset( $_POST['anydress_measurements_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['anydress_measurements_nonce'] ) ), 'save_measurements_action' ) ) {
     127                wc_add_notice( __( 'Security check failed. Please try again.', 'anydress-ai-virtual-try-on' ), 'error' );
     128                return;
     129            }
     130
     131            $user_id = get_current_user_id();
     132            $fields  = $this->get_all_shopper_fields();
     133            $unit    = ( isset( $_POST['anydress_unit'] ) && sanitize_text_field( wp_unslash( $_POST['anydress_unit'] ) ) === 'in' ) ? 'in' : 'cm';
     134
     135            $clean = array();
     136            if ( isset( $_POST['anydress_m'] ) && is_array( $_POST['anydress_m'] ) ) {
     137                // Values are numeric strings; validated numerically field-by-field below
     138                $raw = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['anydress_m'] ) );
     139                foreach ( $fields as $key => $def ) {
     140                    if ( isset( $raw[ $key ] ) && $raw[ $key ] !== '' && is_numeric( $raw[ $key ] ) ) {
     141                        $val = (float) $raw[ $key ];
     142                        if ( $val <= 0 ) {
     143                            continue;
     144                        }
     145                        // mm fields (face) are always entered in mm; cm fields follow the chosen unit
     146                        if ( $def['unit'] === 'mm' ) {
     147                            if ( $val <= 400 ) {
     148                                $clean[ $key ] = round( $val, 1 );
     149                            }
     150                        } else {
     151                            $metric = ( $unit === 'in' ) ? round( $val * 2.54, 1 ) : round( $val, 1 );
     152                            if ( $metric <= 350 ) {
     153                                $clean[ $key ] = $metric;
     154                            }
     155                        }
     156                    }
     157                }
     158            }
     159
     160            if ( ! empty( $clean ) ) {
     161                update_user_meta( $user_id, 'anydress_shopper_measurements', $clean );
     162                update_user_meta( $user_id, 'anydress_shopper_unit', $unit );
     163                wc_add_notice( __( 'Your body measurements have been saved.', 'anydress-ai-virtual-try-on' ), 'success' );
     164            } else {
     165                delete_user_meta( $user_id, 'anydress_shopper_measurements' );
     166                delete_user_meta( $user_id, 'anydress_shopper_unit' );
     167                wc_add_notice( __( 'Your body measurements have been removed.', 'anydress-ai-virtual-try-on' ), 'success' );
     168            }
     169
    92170            wp_safe_redirect( wc_get_endpoint_url( 'digital-mannequin', '', wc_get_page_permalink( 'myaccount' ) ) );
    93171            exit;
    … …  
    152230            echo '<a href="' . esc_url( $shop_url ) . '" style="display: inline-block; background: #222; color: #fff; padding: 10px 20px; text-decoration: none; border-radius: 4px;">' . esc_html__( 'Go to Shop', 'anydress-ai-virtual-try-on' ) . '</a>';
    153231            echo '</div>';
    154            
    155             echo '</div>';
    156         }
     232
     233            echo '</div>';
     234        }
     235
     236        $this->render_measurements_section( $user_id );
     237    }
     238
     239    /**
     240     * The "My Body Measurements" edit form inside the My Account tab.
     241     */
     242    private function render_measurements_section( $user_id ) {
     243        $fields = $this->get_all_shopper_fields();
     244        if ( empty( $fields ) ) {
     245            return;
     246        }
     247
     248        $saved = get_user_meta( $user_id, 'anydress_shopper_measurements', true );
     249        if ( ! is_array( $saved ) ) {
     250            $saved = array();
     251        }
     252        $unit = get_user_meta( $user_id, 'anydress_shopper_unit', true );
     253        if ( ! in_array( $unit, array( 'cm', 'in' ), true ) ) {
     254            $unit = 'cm';
     255        }
     256
     257        echo '<h3 style="margin-top: 40px;">' . esc_html__( '📏 My Body Measurements', 'anydress-ai-virtual-try-on' ) . '</h3>';
     258        echo '<p style="color: #666; font-size: 14px;">' . esc_html__( 'Used to make your Virtual Try-On fit as realistic as possible. All fields are optional. Like your mannequin photos, measurements are automatically deleted 7 days after your last mannequin upload.', 'anydress-ai-virtual-try-on' ) . '</p>';
     259
     260        echo '<form method="post" style="background: #fff; padding: 20px; border: 1px solid #e2e2e2; border-radius: 8px;">';
     261        wp_nonce_field( 'save_measurements_action', 'anydress_measurements_nonce' );
     262
     263        // Unit selector
     264        echo '<p style="margin-top: 0;"><strong>' . esc_html__( 'Units:', 'anydress-ai-virtual-try-on' ) . '</strong> ';
     265        echo '<label style="margin-right: 12px;"><input type="radio" name="anydress_unit" value="cm" ' . checked( $unit, 'cm', false ) . ' /> cm</label>';
     266        echo '<label><input type="radio" name="anydress_unit" value="in" ' . checked( $unit, 'in', false ) . ' /> in</label>';
     267        echo ' <small style="color: #888;">' . esc_html__( '(face measurements for eyewear are always in mm)', 'anydress-ai-virtual-try-on' ) . '</small></p>';
     268
     269        echo '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px;">';
     270        foreach ( $fields as $key => $def ) {
     271            $metric_val  = isset( $saved[ $key ] ) ? (float) $saved[ $key ] : '';
     272            $display_val = $metric_val;
     273            $unit_label  = $def['unit'];
     274
     275            if ( $def['unit'] === 'cm' ) {
     276                $unit_label = $unit;
     277                if ( $metric_val !== '' && $unit === 'in' ) {
     278                    $display_val = round( $metric_val / 2.54, 1 );
     279                }
     280            }
     281
     282            echo '<div>';
     283            echo '<label style="display: block; font-size: 13px; font-weight: 600; margin-bottom: 3px;">' . esc_html( $def['label'] ) . ' <small style="font-weight: normal; color: #888;">(' . esc_html( $unit_label ) . ')</small></label>';
     284            echo '<input type="number" step="0.1" min="0" name="anydress_m[' . esc_attr( $key ) . ']" value="' . esc_attr( $display_val ) . '" style="width: 100%; padding: 6px 8px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box;" />';
     285            echo '</div>';
     286        }
     287        echo '</div>';
     288
     289        echo '<div style="margin-top: 15px;">';
     290        echo '<button type="submit" name="anydress_save_measurements" value="1" style="background-color: #222; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: bold;">' . esc_html__( 'Save Measurements', 'anydress-ai-virtual-try-on' ) . '</button>';
     291        echo ' <small style="color: #888; margin-left: 10px;">' . esc_html__( 'Clear all fields and save to delete your measurements.', 'anydress-ai-virtual-try-on' ) . '</small>';
     292        echo '</div>';
     293
     294        echo '</form>';
    157295    }
    158296}
  • anydress-ai-virtual-try-on/tags/1.3.1/includes/frontend/class-anydress-widget.php

    r3530062 r3612360  
    7070            'productId' => $product_id,
    7171            'nonce'     => wp_create_nonce( 'anydress_frontend_action' ),
     72            'sizing'    => $this->get_sizing_context( $product_id ),
    7273            'i18n'      => array(
    7374                'uploadFace'     => __( 'Please upload your face photo.', 'anydress-ai-virtual-try-on' ),
    7475                'faceCompressed' => __( '⚠️ Your face photo could not be compressed enough. Please try a different image.', 'anydress-ai-virtual-try-on' ),
    7576                'bodyCompressed' => __( '⚠️ Your body photo could not be compressed enough. Please try a different image.', 'anydress-ai-virtual-try-on' ),
    76                 'errorProcess'   => __( 'Error processing images. Please try different photos.', 'anydress-ai-virtual-try-on' )
     77                'errorProcess'   => __( 'Error processing images. Please try different photos.', 'anydress-ai-virtual-try-on' ),
     78                'fitWarning'     => __( 'Fit note:', 'anydress-ai-virtual-try-on' )
    7779            )
    7880        ));
     81    }
     82
     83    /**
     84     * Build the sizing context for the frontend JS.
     85     * Returns null when the product has no size chart assigned (plugin behaves exactly as before).
     86     * NOTE: garment measurements themselves never leave the server — only size labels go to the browser.
     87     */
     88    private function get_sizing_context( $product_id ) {
     89        if ( ! function_exists( 'anydress_size_get_product_chart' ) ) {
     90            return null;
     91        }
     92
     93        $chart = anydress_size_get_product_chart( $product_id );
     94        if ( ! $chart ) {
     95            return null;
     96        }
     97
     98        $type_def = anydress_size_get_type( $chart['product_type'] );
     99        if ( ! $type_def ) {
     100            return null;
     101        }
     102
     103        $size_labels = array();
     104        foreach ( $chart['rows'] as $row ) {
     105            $size_labels[] = $row['label'];
     106        }
     107
     108        // Locate the size attribute so JS can sync with the variation dropdown
     109        $size_attr_field = '';
     110        $product = wc_get_product( $product_id );
     111        $size_attr = anydress_size_get_product_size_attribute( $product );
     112        if ( $size_attr ) {
     113            $size_attr_field = 'attribute_' . sanitize_title( $size_attr['attribute_name'] );
     114        }
     115
     116        // Saved shopper measurements (stored in metric)
     117        $user_id = get_current_user_id();
     118        $saved   = $user_id ? get_user_meta( $user_id, 'anydress_shopper_measurements', true ) : array();
     119        if ( ! is_array( $saved ) ) {
     120            $saved = array();
     121        }
     122        $saved_unit = $user_id ? get_user_meta( $user_id, 'anydress_shopper_unit', true ) : '';
     123        if ( ! in_array( $saved_unit, array( 'cm', 'in' ), true ) ) {
     124            $saved_unit = 'cm';
     125        }
     126
     127        return array(
     128            'productType'    => $chart['product_type'],
     129            'metricUnit'     => $type_def['unit'],          // what the server expects (cm or mm)
     130            'allowImperial'  => (bool) $type_def['allow_imperial'],
     131            'preferredUnit'  => $saved_unit,
     132            'sizeLabels'     => $size_labels,
     133            'sizeAttrField'  => $size_attr_field,
     134            'shopperFields'  => $type_def['shopper_fields'],
     135            'saved'          => (object) $saved,
     136        );
    79137    }
    80138
    … …  
    193251        echo '        </div>';
    194252
     253        // --- SIZING & FIT SECTION (only when the product has a size chart) ---
     254        // Note: get_queried_object_id() is reliable in wp_footer, unlike get_the_ID() after custom loops
     255        $sizing = $this->get_sizing_context( get_queried_object_id() );
     256        if ( $sizing ) {
     257            $this->render_fit_section( $sizing );
     258        }
     259
    195260        echo '        <div id="anydress-result-area"></div>';
    196261
    … …  
    198263        echo '</div>';
    199264    }
     265
     266    /**
     267     * Render the optional "Get an accurate fit" block inside the modal.
     268     * Works for both the saved-mannequin flow and the new-upload flow.
     269     */
     270    private function render_fit_section( $sizing ) {
     271        $is_mm = ( $sizing['metricUnit'] === 'mm' );
     272
     273        echo '<div id="anydress-fit-section" class="anydress-fit-section">';
     274        echo '  <details id="anydress-fit-details">';
     275        echo '      <summary>' . esc_html__( '📏 Get an accurate fit (optional)', 'anydress-ai-virtual-try-on' ) . '</summary>';
     276        echo '      <div class="anydress-fit-body">';
     277
     278        // 1. Size picker (synced with the variation dropdown when one exists)
     279        echo '          <div class="anydress-fit-row">';
     280        echo '              <label for="anydress-fit-size">' . esc_html__( 'Size you\'re trying on', 'anydress-ai-virtual-try-on' ) . '</label>';
     281        echo '              <select id="anydress-fit-size">';
     282        echo '                  <option value="">' . esc_html__( '— Select size —', 'anydress-ai-virtual-try-on' ) . '</option>';
     283        foreach ( $sizing['sizeLabels'] as $label ) {
     284            echo '              <option value="' . esc_attr( $label ) . '">' . esc_html( $label ) . '</option>';
     285        }
     286        echo '              </select>';
     287        echo '          </div>';
     288
     289        // 2. Unit toggle (hidden for mm-based products like eyewear)
     290        if ( ! $is_mm && $sizing['allowImperial'] ) {
     291            echo '      <div class="anydress-fit-row anydress-fit-unit-row">';
     292            echo '          <label>' . esc_html__( 'Units', 'anydress-ai-virtual-try-on' ) . '</label>';
     293            echo '          <span>';
     294            echo '              <label class="anydress-fit-unit-opt"><input type="radio" name="anydress_fit_unit" value="cm" ' . checked( $sizing['preferredUnit'], 'cm', false ) . ' /> cm</label>';
     295            echo '              <label class="anydress-fit-unit-opt"><input type="radio" name="anydress_fit_unit" value="in" ' . checked( $sizing['preferredUnit'], 'in', false ) . ' /> in</label>';
     296            echo '          </span>';
     297            echo '      </div>';
     298        }
     299
     300        // 3. Measurement inputs (values injected by JS from saved data, converted to the active unit)
     301        echo '          <p class="anydress-fit-hint">' . esc_html__( 'Your measurements are optional — the more you fill in, the more realistic the generated fit. They are saved with your digital mannequin and automatically deleted after 7 days.', 'anydress-ai-virtual-try-on' ) . '</p>';
     302        echo '          <div class="anydress-fit-fields">';
     303        foreach ( $sizing['shopperFields'] as $key => $label ) {
     304            $unit_suffix = $is_mm ? 'mm' : ''; // body-field suffix is drawn by JS (follows the toggle)
     305            echo '      <div class="anydress-fit-field">';
     306            echo '          <label for="anydress-fit-' . esc_attr( $key ) . '">' . esc_html( $label ) . ( $unit_suffix ? ' <small>(' . esc_html( $unit_suffix ) . ')</small>' : ' <small class="anydress-fit-unit-label"></small>' );
     307            echo '          </label>';
     308            echo '          <input type="number" step="0.1" min="0" inputmode="decimal" id="anydress-fit-' . esc_attr( $key ) . '" class="anydress-fit-input" data-key="' . esc_attr( $key ) . '" data-fixed-unit="' . esc_attr( $is_mm ? 'mm' : '' ) . '" />';
     309            echo '      </div>';
     310        }
     311        echo '          </div>';
     312
     313        echo '      </div>';
     314        echo '  </details>';
     315        echo '</div>';
     316    }
    200317}
    201318
  • anydress-ai-virtual-try-on/tags/1.3.1/readme.txt

    r3530142 r3612360  
    1 === AnyDress AI Virtual Try-On ===
     1=== AnyDress – AI Virtual Try-On for WooCommerce & AI Product Photos ===
    22Contributors: anydress
    3 Tags: woocommerce, virtual try on, ai, fashion, ecommerce
     3Tags: virtual try on, woocommerce, ai, product images, clothing
    44Requires at least: 5.8
    5 Tested up to: 6.9
    6 Stable tag: 1.0.0
     5Tested up to: 7.0
     6Stable tag: 1.3.1
    77Requires PHP: 7.4
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 Elevate your digital boutique with seamless AI virtual try-on and instant studio-quality catalog photoshoots.
     11AI virtual try-on for WooCommerce: shoppers try on clothes on their own photo, and you generate studio-quality product images without models.
    1212
    1313== Description ==
    1414
    15 AnyDress AI Virtual Try-On revolutionizes your WooCommerce store by allowing shoppers to visually "try on" your clothing using artificial intelligence. Increase conversion rates, boost buyer confidence, and reduce return rates by letting customers see exactly how your garments will look on their own bodies.
     15**AnyDress adds an AI virtual fitting room to your WooCommerce store — and an AI photo studio to your dashboard.** Shoppers can virtually try on clothes using their own photos before they buy, and store owners can generate professional product images without models, photographers, or physical photoshoots.
    1616
    17 Additionally, store owners can use the Virtual Photoshoot dashboard to generate professional, studio-quality catalog images of their garments using AI models or ghost mannequins, saving thousands of dollars on physical photography.
     17One plugin, two revenue levers: higher conversions from confident shoppers, and lower costs from AI-generated catalog photography.
    1818
    19 = Features =
    20 *   **Shopper Virtual Try-On:** Adds a customizable "Try It On" button to WooCommerce product pages.
    21 *   **Digital Mannequin:** Logged-in shoppers can securely save their face and body photos for 1-click try-ons across your whole store.
    22 *   **Admin Virtual Photoshoot:** Instantly generate new product catalog images using WooCommerce products or raw garment uploads.
    23 *   **AI Models & Ghost Mannequins:** Choose to use the original model, upload your own, generate a random AI model, or create invisible ghost mannequins.
    24 *   **Custom Backgrounds:** Describe any scene or background to place your models in.
    25 *   **Magic Edit:** Alter the pose, aspect ratio, resolution, and photography style of your generated images.
    26 *   **Usage Controls:** Set daily try-on limits to protect your API budget.
    27 *   **Privacy-First Design:** Built-in legal consent checkboxes for both shoppers and admins, ensuring no data is sent to the AI engine without explicit user agreement.
     19= A virtual fitting room for your shoppers =
     20
     21A customizable **"Try It On" button** appears on your product pages, right next to Add to Cart. Shoppers upload a photo (or reuse their saved Digital Mannequin) and see exactly how the garment looks on their own body — not on a model who looks nothing like them.
     22
     23* **1-click try-ons across your whole store** — logged-in shoppers securely save face and body photos once, then try on anything.
     24* **True-to-size fit** — pair try-ons with real garment measurements so shoppers see (or get warned about) sizes that run tight or loose. Fewer sizing mistakes, fewer returns, less bracketing.
     25* **Works with your theme** — hooks into standard WooCommerce templates automatically.
     26
     27= An AI photo studio for store owners =
     28
     29The **Virtual Photoshoot dashboard** turns a flat lay, hanger shot, or plain packshot into studio-quality on-model imagery in minutes:
     30
     31* Generate catalog images from your existing WooCommerce product photos or raw garment uploads.
     32* Choose an **AI model**, upload your own model, or create clean **ghost mannequin** shots.
     33* Describe any **custom background** or scene in plain words.
     34* **Magic Edit** — change pose, aspect ratio, resolution, and photography style after generation.
     35
     36No studio rental, no model fees, no reshoots when your catalog changes.
     37
     38= Built-in size charts =
     39
     40Create reusable size charts (tops, bottoms, dresses, outerwear, swimwear, eyewear, headwear) with real garment measurements per size. Assign them to products in bulk, override values per product, and let shoppers optionally add their own measurements (cm or inches) for a realistic fit.
     41
     42= Privacy-first by design =
     43
     44* Explicit consent checkboxes for both shoppers and admins — no data reaches the AI engine without agreement.
     45* Shopper photos live on **your own server**, auto-deleted after 7 days; shoppers can delete their data anytime from My Account.
     46* Images are processed in memory and discarded by the API immediately after generation — never used to train AI models.
     47
     48= Perfect for =
     49
     50* [Apparel & clothing stores](https://anydress.ai/use-cases/apparel/)
     51* [Eyewear & glasses](https://anydress.ai/use-cases/eyewear/)
     52* [Baby & kids clothes](https://anydress.ai/use-cases/baby-clothes/)
     53* [Jewelry](https://anydress.ai/use-cases/jewelry/)
     54* [Other use cases](https://anydress.ai/use-cases/)
     55
     56= All features =
     57
     58* **Shopper Virtual Try-On:** customizable "Try It On" button on WooCommerce product pages.
     59* **Digital Mannequin:** shoppers securely save photos for 1-click try-ons store-wide.
     60* **Admin Virtual Photoshoot:** generate product catalog images from products or raw garment uploads.
     61* **AI Models & Ghost Mannequins:** original model, your own model, a generated AI model, or invisible ghost mannequins.
     62* **Custom Backgrounds:** describe any scene to place your models in.
     63* **Magic Edit:** alter pose, aspect ratio, resolution, and photography style.
     64* **Size Charts & True-to-Size Fit:** reusable charts with per-size garment measurements, bulk assignment, per-product overrides.
     65* **Shopper Measurements (optional):** private body measurements for realistic fit rendering and sizing advice.
     66* **Usage Controls:** daily try-on limits to protect your API budget.
     67* **Privacy-First Design:** built-in legal consent flows for shoppers and admins.
     68
     69= Learn more =
     70
     71* [How AI virtual try-on works in WooCommerce](https://anydress.ai/virtual-try-on-for-woocommerce-guide/)
     72* [Creating product images without models](https://anydress.ai/ai-clothing-photography-without-a-model/)
     73* [Reducing clothing return rates](https://anydress.ai/reduce-clothing-return-rates-woocommerce/)
     74* [Plans and pricing](https://anydress.ai/#pricing)
    2875
    2976== External Services ==
    … …  
    3279
    3380* What the service is used for: The AnyDress AI API provides the artificial intelligence engine required to "clothe" a digital mannequin with a product image and to generate studio-quality catalog photoshoots.
    34 * What data is sent and when: 
     81* What data is sent and when:
    3582    1. Shopper Try-On: Only after a logged-in user gives explicit consent via a checkbox, their uploaded photos (Face/Body) and the WooCommerce product image are converted to Base64 and sent to the AnyDress AI endpoint.
    36     2. Admin Photoshoot: Only after the administrator confirms rights to the images, the selected garment and model photos are sent to the API.
    37     3. Verification: The store domain and license key are sent to verify subscription status and usage limits.
     83    2. Optional Fit Data: If the shopper voluntarily enters body measurements, those numbers (together with the store's garment measurements for the selected size) are sent alongside the try-on request to render a realistic fit. Measurements are never required.
     84    3. Admin Photoshoot: Only after the administrator confirms rights to the images, the selected garment and model photos are sent to the API.
     85    4. Verification & Task Status: The store domain, license key, and generation task IDs are sent to verify subscription status, usage limits, and to retrieve finished images.
    3886* Data Handling & Privacy: Images are processed in secure server memory to generate the result and are immediately discarded by the API upon completion. User data is confidential and never used to train AI models.
    3987*   **Terms & Privacy Links:**
    … …  
    53101== Frequently Asked Questions ==
    54102
     103= How does the AI virtual try-on work for shoppers? =
     104A "Try It On" button appears on your product pages. The shopper uploads a photo of themselves (or reuses their saved Digital Mannequin), and the AI renders the garment on their own body in seconds — including realistic fit if measurements and size charts are set up.
     105
     106= Can I really create product images without hiring models or a photographer? =
     107Yes. The Virtual Photoshoot dashboard turns a flat lay, hanger shot, ghost mannequin shot, or plain packshot into on-model, studio-quality catalog images. You choose the model, background, pose, and style.
     108
     109= Does virtual try-on reduce return rates? =
     110Sizing and "it looked different online" are the top reasons clothing gets returned. Letting shoppers see garments on their own body — with optional true-to-size fit warnings — addresses both causes directly. See our [guide to reducing WooCommerce return rates](https://anydress.ai/reduce-clothing-return-rates-woocommerce/).
     111
    55112= Do I need an AnyDress AI account to use this plugin? =
    56 Yes, you need an active API License Key from AnyDress AI to connect your WooCommerce store to the AI generation engine.
     113Yes, you need an active API License Key from AnyDress AI to connect your WooCommerce store to the AI generation engine. Plans are listed at https://anydress.ai/#pricing.
     114
     115= Does it work for eyewear, accessories, or kids' clothes? =
     116Yes — size charts cover tops, bottoms, dresses, outerwear, swimwear, eyewear, and headwear, and stores use AnyDress for apparel, glasses, baby and kids' clothes, and jewelry.
    57117
    58118= Where are shopper photos saved? =
    59119Shopper photos are securely saved in a hidden, local folder on your own WordPress server (`wp-content/uploads/anydress-mannequins/`). To protect privacy and save disk space, a cron job automatically deletes shopper photos 7 days after upload. Shoppers can also manually delete their data at any time via the "Digital Mannequin" tab in their My Account page.
     120
     121= Are shopper body measurements required? Where are they stored? =
     122No — measurements are entirely optional; without them the try-on works exactly as before. If provided, they are stored on your own WordPress server as user meta, editable and deletable by the shopper in My Account, and automatically removed by the same 7-day cleanup that deletes mannequin photos (and on plugin uninstall).
     123
     124= How do I set up sizing for my products? =
     125Go to **AnyDress AI → Size Charts**, create a chart with your garment measurements per size, and assign it to products either from the chart itself or in the product editor under **Product data → AnyDress Sizing** (where you can also override individual values per product).
    60126
    61127= Does this work with any theme? =
    … …  
    64130== Screenshots ==
    65131
    66 1. The Virtual Try-On button on a WooCommerce product page.
    67 2. The shopper upload modal.
    68 3. The shopper Virtual Try-On result.
    69 4. The Admin Virtual Photoshoot dashboard.
     1321. The "Try It On" button on a WooCommerce product page — shoppers start a virtual try-on next to Add to Cart.
     1332. The shopper upload modal with consent and Digital Mannequin options.
     1343. A finished virtual try-on: the garment rendered on the shopper's own photo.
     1354. The Admin Virtual Photoshoot dashboard — AI product images without models.
    701365. AnyDress plugin settings and API dashboard.
    71137
    72138== Changelog ==
    73139
     140= 1.3.1 =
     141* Hardened input sanitization and declared compatibility with WordPress 7.0 (Plugin Check compliance).
     142
     143= 1.3.0 =
     144* New settings: choose the Virtual Try-On aspect ratio (2:3, 3:4, 4:5) and resolution (1K or 2K) so every shopper result has a consistent, predictable format.
     145* Friendlier sizing feedback: when a selected size doesn't fit the shopper's measurements, the try-on now shows a helpful sizing-advice notice (instead of an error) and opens the size picker so they can switch and retry.
     146
     147= 1.2.0 =
     148* Faster, more reliable generations: try-ons are now processed asynchronously — the page shows progress and delivers the image the moment it is ready, with no long-hanging requests.
     149* Improved reliability on slow hosts: generation no longer depends on a single long HTTP request.
     150
     151= 1.1.1 =
     152* Size Charts: assign a chart to many products at once with a searchable product picker, and duplicate charts for product models with different measurements.
     153
     154= 1.1.0 =
     155* New Size Charts system: reusable per-size garment measurements for tops, bottoms, dresses, outerwear, swimwear, eyewear, and headwear.
     156* Optional shopper body measurements (cm/inches) for a true-to-size virtual try-on, with a store setting to block or realistically render ill-fitting sizes.
     157* Per-product measurement overrides and automatic matching with WooCommerce size attributes.
     158* Fixed an issue where the face photo was uploaded twice, which could exceed server upload limits.
     159
    74160= 1.0.0 =
    75161* Initial release.
  • anydress-ai-virtual-try-on/tags/1.3.1/uninstall.php

    r3530062 r3612360  
    2323    'anydress_checkbox_tick_color',
    2424    'anydress_privacy_page_id', // Legacy setting
    25     'anydress_plugin_version'
     25    'anydress_plugin_version',
     26    'anydress_privacy_policy_url',
     27    'anydress_fit_mode',
     28    'anydress_size_charts'
    2629);
    2730
    … …  
    4245    'anydress_mannequin_upload_time',
    4346    'anydress_usage_date',
    44     'anydress_usage_count'
     47    'anydress_usage_count',
     48    'anydress_shopper_measurements',
     49    'anydress_shopper_unit'
    4550);
    4651
    … …  
    5055    delete_metadata( 'user', 0, $anydress_meta_key, '', true );
    5156}
     57
     58// 2b. Delete product sizing meta for ALL products
     59delete_metadata( 'post', 0, '_anydress_size_chart_id', '', true );
     60delete_metadata( 'post', 0, '_anydress_size_overrides', '', true );
    5261
    5362// 3. Delete the custom mannequins folder and all images inside it
  • anydress-ai-virtual-try-on/trunk/anydress-ai-virtual-try-on.php

    r3530062 r3612360  
    44 * Plugin URI: https://anydress.ai/plugin/
    55 * Description: Elevate your digital boutique with seamless AI virtual try-on.
    6  * Version: 1.0.0
     6 * Version: 1.3.1
    77 * Author: AnyDress.ai
    88 * Author URI: https://anydress.ai
    … …  
    2020
    2121// Define plugin constants for easy reference later
    22 define( 'ANYDRESS_VERSION', '1.0.0' ); // Added version for cache-busting CSS/JS
     22define( 'ANYDRESS_VERSION', '1.3.1' ); // Added version for cache-busting CSS/JS
    2323define( 'ANYDRESS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2424define( 'ANYDRESS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    … …  
    5050    }
    5151   
     52    // 0. Load the shared Size Config (needed by Admin UI, AJAX, and Frontend alike)
     53    require_once ANYDRESS_PLUGIN_DIR . 'includes/anydress-size-config.php';
     54
    5255    // 1. Load Admin UI (Only in WP Admin, but NOT during AJAX)
    5356    if ( is_admin() && ! wp_doing_ajax() ) {
    5457        require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-settings.php';
    5558        require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-photoshoot.php';
     59        require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-size-charts.php';
     60        require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/class-anydress-product-sizes.php';
    5661    }
    5762
    5863    // 2. Load AJAX Handlers (Only when an AJAX request is firing)
    5964    if ( wp_doing_ajax() ) {
     65        // Async task status relay (shared by frontend + admin)
     66        require_once ANYDRESS_PLUGIN_DIR . 'includes/anydress-task-client.php';
    6067        // Admin AJAX
    6168        require_once ANYDRESS_PLUGIN_DIR . 'includes/admin/ajax/ajax-generate-photoshoot.php';
  • anydress-ai-virtual-try-on/trunk/assets/css/frontend-tryon.css

    r3530062 r3612360  
    127127    border-color: #222; /* Default border */
    128128}
     129/* ==========================================================================
     130   Sizing & Fit section (optional accurate-fit inputs)
     131   ========================================================================== */
     132.anydress-fit-section {
     133    margin: 18px 0 5px;
     134    text-align: left;
     135}
     136
     137.anydress-fit-section details {
     138    border: 1px solid #e2e2e2;
     139    border-radius: 8px;
     140    background: rgba(255, 255, 255, 0.6);
     141}
     142
     143.anydress-fit-section summary {
     144    padding: 12px 15px;
     145    cursor: pointer;
     146    font-weight: 600;
     147    font-size: 14px;
     148    user-select: none;
     149}
     150
     151.anydress-fit-body {
     152    padding: 5px 15px 15px;
     153    border-top: 1px solid #eee;
     154}
     155
     156.anydress-fit-row {
     157    display: flex;
     158    align-items: center;
     159    gap: 12px;
     160    margin: 10px 0;
     161    font-size: 13px;
     162}
     163
     164.anydress-fit-row > label {
     165    min-width: 140px;
     166    font-weight: 600;
     167}
     168
     169.anydress-fit-row select {
     170    padding: 6px 10px;
     171    border: 1px solid #ccc;
     172    border-radius: 6px;
     173    max-width: 180px;
     174}
     175
     176.anydress-fit-unit-opt {
     177    margin-right: 12px;
     178    cursor: pointer;
     179}
     180
     181.anydress-fit-hint {
     182    font-size: 12px;
     183    color: #777;
     184    margin: 8px 0 10px;
     185    line-height: 1.4;
     186}
     187
     188.anydress-fit-fields {
     189    display: grid;
     190    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
     191    gap: 10px;
     192}
     193
     194.anydress-fit-field label {
     195    display: block;
     196    font-size: 12px;
     197    font-weight: 600;
     198    margin-bottom: 3px;
     199    line-height: 1.3;
     200}
     201
     202.anydress-fit-field small {
     203    font-weight: normal;
     204    color: #888;
     205}
     206
     207.anydress-fit-field input {
     208    width: 100%;
     209    padding: 6px 8px;
     210    border: 1px solid #ccc;
     211    border-radius: 6px;
     212    font-size: 13px;
     213    box-sizing: border-box;
     214}
     215
     216.anydress-fit-warning {
     217    background: #fff8e5;
     218    border-left: 4px solid #dba617;
     219    padding: 10px 14px;
     220    margin: 12px 0;
     221    font-size: 13px;
     222    text-align: left;
     223    border-radius: 0 6px 6px 0;
     224    line-height: 1.4;
     225}
  • anydress-ai-virtual-try-on/trunk/assets/js/admin-photoshoot.js

    r3530062 r3612360  
    1515        });
    1616    }
     17    // ======================================================================
     18    // ASYNC TASK RESOLUTION
     19    // The server accepts jobs in seconds and returns a task_id; we poll a
     20    // lightweight status action until the image is ready (or the task fails).
     21    // ======================================================================
     22    async function anydressResolveTask(data) {
     23        if (!data || !data.success || !data.data || !data.data.task_id) {
     24            return data; // not a task response — pass through unchanged
     25        }
     26        const taskId = data.data.task_id;
     27
     28        for (let i = 0; i < 130; i++) { // ~9 minutes max (server times tasks out at 8)
     29            await new Promise(r => setTimeout(r, i === 0 ? 6000 : 4000));
     30            try {
     31                const fd = new FormData();
     32                fd.append('action', 'anydress_admin_check_task');
     33                fd.append('task_id', taskId);
     34                fd.append('nonce', document.getElementById('anydress_nonce').value);
     35
     36                const resp = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: fd });
     37                const status = await resp.json();
     38
     39                if (status && status.success && status.data && status.data.status === 'pending') {
     40                    continue; // still generating
     41                }
     42                return status; // completed (has image_url) or failed (error shape)
     43            } catch (e) {
     44                // transient network hiccup — keep polling
     45            }
     46        }
     47        return { success: false, data: { message: 'Generation timed out. Please try again.' } };
     48    }
     49
    1750    let originalImageUrl = "";
    1851    let currentIsGhost = false;
    … …  
    436469                let response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData });
    437470                let data = await response.json();
     471                data = await anydressResolveTask(data);
    438472
    439473                // Fallback Interceptor
    … …  
    441475                    const loadingText = loadingBox.querySelector('p');
    442476                    if(loadingText) loadingText.innerText = "Optimizing... Please wait another 20-70 seconds";
    443                    
     477
    444478                    formData.set('force_fallback', 'true');
    445479                    response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData });
    446480                    data = await response.json();
     481                    data = await anydressResolveTask(data);
    447482                }
    448483
    … …  
    662697            let response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData });
    663698            let data = await response.json();
     699            data = await anydressResolveTask(data);
    664700
    665701            // Fallback Interceptor
    666702            if (!data.success && data.data && data.data.error_code === 'require_fallback') {
    667703                loadingText.innerText = "Optimizing... Please wait another 20-70 seconds";
    668                
     704
    669705                formData.set('force_fallback', 'true');
    670706                response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData });
    671707                data = await response.json();
     708                data = await anydressResolveTask(data);
    672709            }
    673710
    … …  
    762799            let response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData });
    763800            let data = await response.json();
     801            data = await anydressResolveTask(data);
    764802
    765803            // Fallback Interceptor
    766804            if (!data.success && data.data && data.data.error_code === 'require_fallback') {
    767805                loadingText.innerText = "Optimizing... Please wait another 20-70 seconds";
    768                
     806
    769807                formData.set('force_fallback', 'true');
    770808                response = await fetch(anydressAdmin.ajaxUrl, { method: 'POST', body: formData });
    771809                data = await response.json();
     810                data = await anydressResolveTask(data);
    772811            }
    773812
  • anydress-ai-virtual-try-on/trunk/assets/js/frontend-tryon.js

    r3530062 r3612360  
    7575    }
    7676
     77    // ======================================================================
     78    // SIZING & FIT (optional accurate-fit section)
     79    // ======================================================================
     80    var sizing = anydressFrontend.sizing || null;
     81    var fitSizeSelect = document.getElementById("anydress-fit-size");
     82
     83    function fitActiveUnit() {
     84        var checked = document.querySelector('input[name="anydress_fit_unit"]:checked');
     85        return checked ? checked.value : "cm";
     86    }
     87
     88    function fitUpdateUnitLabels() {
     89        var unit = fitActiveUnit();
     90        document.querySelectorAll(".anydress-fit-unit-label").forEach(function (el) {
     91            el.textContent = "(" + unit + ")";
     92        });
     93    }
     94
     95    function fitInitInputs() {
     96        if (!sizing) { return; }
     97        var unit = fitActiveUnit();
     98        var saved = sizing.saved || {};
     99        document.querySelectorAll(".anydress-fit-input").forEach(function (input) {
     100            var key = input.getAttribute("data-key");
     101            var metric = saved[key]; // stored in cm (body) or mm (face)
     102            if (typeof metric === "undefined" || metric === null || metric === "") { return; }
     103            metric = parseFloat(metric);
     104            if (!(metric > 0)) { return; }
     105            if (input.getAttribute("data-fixed-unit") === "mm") {
     106                input.value = metric;
     107            } else {
     108                input.value = (unit === "in") ? Math.round((metric / 2.54) * 10) / 10 : metric;
     109            }
     110        });
     111        fitUpdateUnitLabels();
     112    }
     113
     114    // Unit toggle: convert already-entered values between cm and in
     115    document.querySelectorAll('input[name="anydress_fit_unit"]').forEach(function (radio) {
     116        radio.addEventListener("change", function () {
     117            var toUnit = radio.value;
     118            document.querySelectorAll(".anydress-fit-input").forEach(function (input) {
     119                if (input.getAttribute("data-fixed-unit") === "mm") { return; }
     120                var v = parseFloat(input.value);
     121                if (!(v > 0)) { return; }
     122                input.value = (toUnit === "in")
     123                    ? Math.round((v / 2.54) * 10) / 10
     124                    : Math.round(v * 2.54 * 10) / 10;
     125            });
     126            fitUpdateUnitLabels();
     127        });
     128    });
     129
     130    // Sync the size picker with the WooCommerce variation dropdown (if one exists)
     131    function fitSyncFromVariation() {
     132        if (!sizing || !sizing.sizeAttrField || !fitSizeSelect) { return; }
     133        var variationSelect = document.querySelector('select[name="' + sizing.sizeAttrField + '"]');
     134        if (!variationSelect || !variationSelect.value) { return; }
     135        var chosen = variationSelect.selectedOptions.length
     136            ? variationSelect.selectedOptions[0].textContent.trim()
     137            : variationSelect.value;
     138        var candidates = [variationSelect.value.toLowerCase(), chosen.toLowerCase()];
     139        for (var i = 0; i < fitSizeSelect.options.length; i++) {
     140            var optVal = fitSizeSelect.options[i].value.toLowerCase().trim();
     141            if (optVal !== "" && candidates.indexOf(optVal) !== -1) {
     142                fitSizeSelect.value = fitSizeSelect.options[i].value;
     143                return;
     144            }
     145        }
     146    }
     147
     148    if (sizing && sizing.sizeAttrField) {
     149        var wooSizeSelect = document.querySelector('select[name="' + sizing.sizeAttrField + '"]');
     150        if (wooSizeSelect) {
     151            wooSizeSelect.addEventListener("change", fitSyncFromVariation);
     152        }
     153    }
     154
     155    if (sizing) {
     156        fitInitInputs();
     157        fitSyncFromVariation();
     158    }
     159
     160    // Append the shopper's sizing choices to an outgoing request
     161    function fitAppendToForm(formData) {
     162        if (!sizing) { return; }
     163
     164        if (fitSizeSelect && fitSizeSelect.value) {
     165            formData.append("size_label", fitSizeSelect.value);
     166        }
     167        formData.append("measure_unit", fitActiveUnit());
     168
     169        var any = false;
     170        document.querySelectorAll(".anydress-fit-input").forEach(function (input) {
     171            var v = parseFloat(input.value);
     172            if (v > 0) {
     173                formData.append("measurements[" + input.getAttribute("data-key") + "]", v);
     174                any = true;
     175            }
     176        });
     177
     178        if (any || (fitSizeSelect && fitSizeSelect.value)) {
     179            formData.append("sizing_active", "true");
     180        }
     181    }
     182
     183    // ======================================================================
     184    // ASYNC TASK RESOLUTION
     185    // The server now accepts the job in seconds and returns a task_id; we poll
     186    // a lightweight status action until the image is ready (or the task fails).
     187    // ======================================================================
     188    async function anydressResolveTask(data) {
     189        if (!data || !data.success || !data.data || !data.data.task_id) {
     190            return data; // not a task response — pass through unchanged
     191        }
     192        var taskId = data.data.task_id;
     193
     194        for (var i = 0; i < 130; i++) { // ~9 minutes max (server times tasks out at 8)
     195            await new Promise(function (r) { setTimeout(r, i === 0 ? 6000 : 4000); });
     196            try {
     197                var fd = new FormData();
     198                fd.append("action", "anydress_check_task");
     199                fd.append("task_id", taskId);
     200                fd.append("nonce", anydressFrontend.nonce);
     201
     202                var resp = await fetch(anydressFrontend.ajaxUrl, { method: "POST", body: fd });
     203                var status = await resp.json();
     204
     205                if (status && status.success && status.data && status.data.status === "pending") {
     206                    continue; // still generating
     207                }
     208                return status; // completed (has image_url) or failed (error shape)
     209            } catch (e) {
     210                // transient network hiccup — keep polling
     211            }
     212        }
     213        return { success: false, data: { message: "Generation timed out. Please try again." } };
     214    }
     215
    77216    // Central function to send AJAX request
    78217    async function sendToEngine(formData, buttonToDisable) {
    … …  
    80219            formData.append("force_fallback", "false");
    81220        }
     221        fitAppendToForm(formData);
    82222
    83223        buttonToDisable.innerText = "Processing... (This takes 20-70 seconds)";
    … …  
    95235            let response = await fetch(anydressFrontend.ajaxUrl, { method: "POST", body: formData });
    96236            let data = await response.json();
     237            data = await anydressResolveTask(data);
    97238
    98239            if (!data.success && data.data && data.data.error_code === 'require_fallback') {
    99240                resultArea.innerHTML = "<p><i>Optimizing... Please wait another 20-70 seconds</i></p>";
    100241                buttonToDisable.innerText = "Optimizing...";
    101                
     242
    102243                formData.set("force_fallback", "true");
    103244                response = await fetch(anydressFrontend.ajaxUrl, { method: "POST", body: formData });
    104245                data = await response.json();
     246                data = await anydressResolveTask(data);
    105247            }
    106248
    … …  
    128270                }
    129271
     272                // Optional fit warning coming back from the sizing engine
     273                var fitWarningHtml = "";
     274                if (data.data.fit_warning) {
     275                    var warnText = document.createElement("div");
     276                    warnText.textContent = data.data.fit_warning; // XSS-safe
     277                    fitWarningHtml = "<div class='anydress-fit-warning'>⚠️ <b>" + (anydressFrontend.i18n.fitWarning || "Fit note:") + "</b> " + warnText.innerHTML + "</div>";
     278                }
     279
    130280                // Display the image
    131                 resultArea.innerHTML = "<h3>Your Virtual Try-On:</h3><img src='" + data.data.image_url + "' style='max-width:100%; max-height:60vh; object-fit:contain; display:block; margin: 15px auto 0; border-radius:8px; box-shadow: 0 4px 10px rgba(0,0,0,0.1);' />";
     281                resultArea.innerHTML = "<h3>Your Virtual Try-On:</h3>" + fitWarningHtml + "<img src='" + data.data.image_url + "' style='max-width:100%; max-height:60vh; object-fit:contain; display:block; margin: 15px auto 0; border-radius:8px; box-shadow: 0 4px 10px rgba(0,0,0,0.1);' />";
    132282            } else {
    133283                // Error: Re-enable button and SHOW the link again
    134                 buttonToDisable.innerText = "Try Again";
    135284                buttonToDisable.disabled = false;
    136285                if (showUploadLink) showUploadLink.style.display = "";
    137                
    138                 // Handle object vs string error messages cleanly
     286
     287                // Handle object vs string error messages cleanly (XSS-safe)
    139288                let errorMsg = typeof data.data === 'string' ? data.data : (data.data.message || 'Unknown error');
    140                 resultArea.innerHTML = "<p style='color:red; font-weight:bold;'>Error: " + errorMsg + "</p>";
     289                var safeMsg = document.createElement("div");
     290                safeMsg.textContent = errorMsg;
     291
     292                // Fit-advice blocks are helpful sizing guidance, not real errors —
     293                // render them as a friendly amber notice instead of red error text.
     294                var fitCodes = ["size_too_small", "size_too_large", "size_length_issue",
     295                    "frame_too_narrow", "frame_too_wide", "bridge_too_narrow", "bridge_too_wide",
     296                    "arms_too_short", "arms_too_long", "headwear_too_small", "headwear_too_large"];
     297                var errCode = (data.data && data.data.error_code) ? data.data.error_code : "";
     298
     299                if (fitCodes.indexOf(errCode) !== -1) {
     300                    buttonToDisable.innerText = "Try Another Size";
     301                    resultArea.innerHTML = "<div class='anydress-fit-warning'>📏 <b>Sizing advice:</b> " + safeMsg.innerHTML + "</div>";
     302                    // Draw the shopper's eye to the size picker so they can switch and retry
     303                    var fitDetails = document.getElementById("anydress-fit-details");
     304                    if (fitDetails) { fitDetails.open = true; }
     305                } else {
     306                    buttonToDisable.innerText = "Try Again";
     307                    resultArea.innerHTML = "<p style='color:red; font-weight:bold;'>Error: " + safeMsg.innerHTML + "</p>";
     308                }
    141309            }
    142310
    … …  
    201369                formData.append("use_saved_mannequin", "false");
    202370                formData.append("face_image", compressedFace);
    203                 formData.append("face_image", compressedFace);
    204371                formData.append("consent_given", "true");
    205372
  • anydress-ai-virtual-try-on/trunk/includes/admin/ajax/ajax-generate-edit.php

    r3530062 r3612360  
    7575        'headers' => array( 'Content-Type' => 'application/json' ),
    7676        'body'    => wp_json_encode( $payload ),
    77         'timeout' => 120
     77        'timeout' => 60
    7878    ));
    7979
    … …  
    9191            wp_send_json_error( array( 'error_code' => 'require_fallback', 'message' => $data['message'] ) );
    9292        }
    93         if ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) {
     93        if ( ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) || ( isset( $data['data']['code'] ) && $data['data']['code'] === 'insufficient_credits' ) ) {
    9494            wp_send_json_error( array( 'error_code' => 'insufficient_credits', 'message' => $data['message'] ) );
    9595        }
    … …  
    9797    }
    9898
    99     if ( empty( $data['image_url'] ) ) wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
     99    // ASYNC: the server returns a task_id; the browser polls 'anydress_admin_check_task'
     100    if ( empty( $data['task_id'] ) ) wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
    100101
    101     wp_send_json_success( array( 'image_url' => $data['image_url'] ) );
     102    wp_send_json_success( array( 'task_id' => sanitize_text_field( $data['task_id'] ), 'status' => 'pending' ) );
    102103}
  • anydress-ai-virtual-try-on/trunk/includes/admin/ajax/ajax-generate-photoshoot.php

    r3530062 r3612360  
    189189        'headers' => array( 'Content-Type' => 'application/json' ),
    190190        'body'    => wp_json_encode( $payload ),
    191         'timeout' => 120
     191        'timeout' => 60
    192192    ));
    193193
    … …  
    206206        }
    207207        // NEW: Gracefully catch the out of credits error so the frontend can display an upgrade prompt
    208         if ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) {
     208        if ( ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) || ( isset( $data['data']['code'] ) && $data['data']['code'] === 'insufficient_credits' ) ) {
    209209            wp_send_json_error( array( 'error_code' => 'insufficient_credits', 'message' => $data['message'] ) );
    210210        }
    … …  
    212212    }
    213213
    214     if ( empty( $data['image_url'] ) ) wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
    215 
    216     wp_send_json_success( array( 'image_url' => $data['image_url'] ) );
     214    // ASYNC: the server returns a task_id; the browser polls 'anydress_admin_check_task'
     215    if ( empty( $data['task_id'] ) ) wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
     216
     217    wp_send_json_success( array( 'task_id' => sanitize_text_field( $data['task_id'] ), 'status' => 'pending' ) );
    217218}
    218219
  • anydress-ai-virtual-try-on/trunk/includes/admin/ajax/ajax-magic-edit.php

    r3530062 r3612360  
    7575        'headers' => array( 'Content-Type' => 'application/json' ),
    7676        'body'    => wp_json_encode( $payload ),
    77         'timeout' => 120
     77        'timeout' => 60
    7878    ));
    7979
    … …  
    9191            wp_send_json_error( array( 'error_code' => 'require_fallback', 'message' => $data['message'] ) );
    9292        }
    93         if ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) {
     93        if ( ( isset( $data['code'] ) && $data['code'] === 'insufficient_credits' ) || ( isset( $data['data']['code'] ) && $data['data']['code'] === 'insufficient_credits' ) ) {
    9494            wp_send_json_error( array( 'error_code' => 'insufficient_credits', 'message' => $data['message'] ) );
    9595        }
    … …  
    9797    }
    9898
    99     if ( empty( $data['image_url'] ) ) wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
     99    // ASYNC: the server returns a task_id; the browser polls 'anydress_admin_check_task'
     100    if ( empty( $data['task_id'] ) ) wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
    100101
    101     wp_send_json_success( array( 'image_url' => $data['image_url'] ) );
     102    wp_send_json_success( array( 'task_id' => sanitize_text_field( $data['task_id'] ), 'status' => 'pending' ) );
    102103}
  • anydress-ai-virtual-try-on/trunk/includes/admin/class-anydress-photoshoot.php

    r3530062 r3612360  
    242242                                <option value="4K"><?php esc_html_e( '4K (Ultra High Definition)', 'anydress-ai-virtual-try-on' ); ?></option>
    243243                            </select>
     244                            <p style="font-size: 11px; color: #888; margin: 6px 0 0;"><?php esc_html_e( 'Note: swimwear, lingerie and similar items are processed by a dedicated engine that tops out at 2K — a 4K request for those items is generated and billed at 2K automatically.', 'anydress-ai-virtual-try-on' ); ?></p>
    244245                        </div>
    245246
  • anydress-ai-virtual-try-on/trunk/includes/admin/class-anydress-settings.php

    r3530062 r3612360  
    279279        );
    280280
     281        register_setting( 'anydress_settings_group', 'anydress_fit_mode', array( $this, 'sanitize_fit_mode' ) );
     282
     283        add_settings_field(
     284            'anydress_fit_mode_field',
     285            __( 'Sizing: Bad Fit Behavior', 'anydress-ai-virtual-try-on' ),
     286            array( $this, 'fit_mode_render' ),
     287            'anydress-settings',
     288            'anydress_settings_section'
     289        );
     290
     291        register_setting( 'anydress_settings_group', 'anydress_vto_aspect', array( $this, 'sanitize_vto_aspect' ) );
     292        register_setting( 'anydress_settings_group', 'anydress_vto_resolution', array( $this, 'sanitize_vto_resolution' ) );
     293
     294        add_settings_field(
     295            'anydress_vto_aspect_field',
     296            __( 'Try-On Image Aspect Ratio', 'anydress-ai-virtual-try-on' ),
     297            array( $this, 'vto_aspect_render' ),
     298            'anydress-settings',
     299            'anydress_settings_section'
     300        );
     301
     302        add_settings_field(
     303            'anydress_vto_resolution_field',
     304            __( 'Try-On Image Resolution', 'anydress-ai-virtual-try-on' ),
     305            array( $this, 'vto_resolution_render' ),
     306            'anydress-settings',
     307            'anydress_settings_section'
     308        );
     309
    281310        // ==========================================
    282311        // 2. APPEARANCE SETTINGS
    … …  
    375404    }
    376405   
     406    public function sanitize_fit_mode( $value ) {
     407        return in_array( $value, array( 'block', 'warn' ), true ) ? $value : 'block';
     408    }
     409
     410    public function sanitize_vto_aspect( $value ) {
     411        return in_array( $value, array( '2:3', '3:4', '4:5' ), true ) ? $value : '3:4';
     412    }
     413
     414    public function sanitize_vto_resolution( $value ) {
     415        return in_array( $value, array( '1K', '2K' ), true ) ? $value : '1K';
     416    }
     417
     418    public function vto_aspect_render() {
     419        $val = get_option( 'anydress_vto_aspect', '3:4' );
     420        $options = array(
     421            '2:3' => __( '2:3 — Classic fashion editorial (tallest)', 'anydress-ai-virtual-try-on' ),
     422            '3:4' => __( '3:4 — Balanced portrait (default)', 'anydress-ai-virtual-try-on' ),
     423            '4:5' => __( '4:5 — Ecommerce / Instagram standard', 'anydress-ai-virtual-try-on' ),
     424        );
     425        echo '<select name="anydress_vto_aspect">';
     426        foreach ( $options as $key => $label ) {
     427            echo '<option value="' . esc_attr( $key ) . '" ' . selected( $val, $key, false ) . '>' . esc_html( $label ) . '</option>';
     428        }
     429        echo '</select>';
     430        echo '<p class="description">' . esc_html__( 'Every shopper try-on image is generated in this fixed portrait format, so results always look consistent across your store.', 'anydress-ai-virtual-try-on' ) . '</p>';
     431    }
     432
     433    public function vto_resolution_render() {
     434        $val = get_option( 'anydress_vto_resolution', '1K' );
     435        echo '<label style="margin-right: 15px;"><input type="radio" name="anydress_vto_resolution" value="1K" ' . checked( $val, '1K', false ) . ' /> ' . esc_html__( '1K — Standard (1 credit rate)', 'anydress-ai-virtual-try-on' ) . '</label>';
     436        echo '<label><input type="radio" name="anydress_vto_resolution" value="2K" ' . checked( $val, '2K', false ) . ' /> ' . esc_html__( '2K — High detail (1.5× credit rate)', 'anydress-ai-virtual-try-on' ) . '</label>';
     437        echo '<p class="description">' . esc_html__( 'Resolution of shopper try-on images. 2K looks sharper on zoom but each try-on costs 1.5× the credits and loads slightly slower on mobile.', 'anydress-ai-virtual-try-on' ) . '</p>';
     438    }
     439
     440    public function fit_mode_render() {
     441        $val = get_option( 'anydress_fit_mode', 'block' );
     442
     443        echo '<label style="display: block; margin-bottom: 8px;">';
     444        echo '<input type="radio" name="anydress_fit_mode" value="block" ' . checked( 'block', $val, false ) . ' /> ';
     445        echo '<strong>' . esc_html__( 'Block generation', 'anydress-ai-virtual-try-on' ) . '</strong> — ' . esc_html__( 'if the shopper\'s measurements say the selected size won\'t fit, skip the image and suggest a better size. Saves credits.', 'anydress-ai-virtual-try-on' );
     446        echo '</label>';
     447
     448        echo '<label style="display: block;">';
     449        echo '<input type="radio" name="anydress_fit_mode" value="warn" ' . checked( 'warn', $val, false ) . ' /> ';
     450        echo '<strong>' . esc_html__( 'Warn but generate', 'anydress-ai-virtual-try-on' ) . '</strong> — ' . esc_html__( 'show a fit warning but still generate the image with a realistically tight or loose fit. Uses credits even for bad fits.', 'anydress-ai-virtual-try-on' );
     451        echo '</label>';
     452
     453        echo '<p class="description">' . wp_kses_post( __( 'Only applies when a product has a <strong>Size Chart</strong> assigned and the shopper provides their body measurements. Manage charts under <strong>AnyDress AI → Size Charts</strong>.', 'anydress-ai-virtual-try-on' ) ) . '</p>';
     454    }
     455
    377456    public function privacy_policy_url_render() {
    378457        $val = get_option( 'anydress_privacy_policy_url', '' );
  • anydress-ai-virtual-try-on/trunk/includes/frontend/ajax/ajax-frontend-tryon.php

    r3530062 r3612360  
    244244
    245245    // ====================================================================
     246    // SIZING & FIT: Build size_data from the store's chart + shopper input
     247    // ====================================================================
     248    $size_data = null;
     249
     250    $sizing_active = isset( $_POST['sizing_active'] ) && $_POST['sizing_active'] === 'true';
     251
     252    if ( $sizing_active && function_exists( 'anydress_size_resolve_garment_data' ) ) {
     253
     254        $size_label = isset( $_POST['size_label'] ) ? sanitize_text_field( wp_unslash( $_POST['size_label'] ) ) : '';
     255        $unit       = ( isset( $_POST['measure_unit'] ) && sanitize_text_field( wp_unslash( $_POST['measure_unit'] ) ) === 'in' ) ? 'in' : 'cm';
     256
     257        // 1. Garment measurements: resolved server-side from the product's size chart.
     258        //    The browser only ever sends the size LABEL — measurements are trusted store data.
     259        $size_data = anydress_size_resolve_garment_data( $product_id, $size_label );
     260
     261        if ( $size_data ) {
     262            // 2. Shopper measurements (optional). Sanitized against the product type's
     263            //    known field list and converted to metric.
     264            $raw_measurements = array();
     265            if ( isset( $_POST['measurements'] ) && is_array( $_POST['measurements'] ) ) {
     266                // Values are numeric strings; numeric validation happens field-by-field
     267                // in anydress_size_sanitize_shopper_measurements() right after.
     268                $raw_measurements = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['measurements'] ) );
     269            }
     270
     271            $shopper = anydress_size_sanitize_shopper_measurements( $raw_measurements, $size_data['product_type'], $unit );
     272
     273            if ( ! empty( $shopper ) ) {
     274                // Persist for reuse (same lifecycle as the mannequin: My Account deletion,
     275                // 7-day cron, uninstall). Merge so switching product types keeps other fields.
     276                $existing = get_user_meta( $user_id, 'anydress_shopper_measurements', true );
     277                if ( ! is_array( $existing ) ) {
     278                    $existing = array();
     279                }
     280                update_user_meta( $user_id, 'anydress_shopper_measurements', array_merge( $existing, $shopper ) );
     281                update_user_meta( $user_id, 'anydress_shopper_unit', $unit );
     282
     283                $size_data = array_merge( $size_data, $shopper );
     284                $size_data['fit_mode'] = anydress_size_get_fit_mode();
     285            } else {
     286                // Size chosen but no body measurements: fit validation is impossible.
     287                // Send nothing so the try-on behaves exactly like the standard flow.
     288                $size_data = null;
     289            }
     290        }
     291    }
     292
     293    // ====================================================================
    246294    // SAAS ROUTING: Send to AnyDress.ai Server
    247295    // ====================================================================
    … …  
    255303
    256304    $payload = array(
    257         'license_key'    => $api_key, 
     305        'license_key'    => $api_key,
    258306        'client_domain'  => $safe_domain,
    259         'user_hash'      => $user_hash, 
     307        'user_hash'      => $user_hash,
    260308        'force_fallback' => $force_fallback,
     309        'aspect_ratio'   => get_option( 'anydress_vto_aspect', '3:4' ),
     310        'resolution'     => get_option( 'anydress_vto_resolution', '1K' ),
    261311        'face_base64'    => $face_base64,
    262312        'face_mime'      => $face_mime,
    … …  
    267317    );
    268318
     319    // Attach sizing payload only when fully resolved (garment + shopper data)
     320    if ( ! empty( $size_data ) ) {
     321        $payload['size_data'] = $size_data;
     322    }
     323
    269324    $response = wp_remote_post( $saas_endpoint, array(
    270325        'headers' => array( 'Content-Type' => 'application/json' ),
    271326        'body'    => wp_json_encode( $payload ),
    272         'timeout' => 120 // 120 seconds to allow for image generation
     327        'timeout' => 60 // Submission returns a task_id in seconds; polling handles the rest
    273328    ));
    274329
    … …  
    284339    // Handle errors returned by the SaaS Server
    285340    if ( $status_code !== 200 ) {
    286         // Propagate the specific fallback error code to frontend shopper
    287         if ( isset( $data['code'] ) && $data['code'] === 'require_fallback' ) {
    288             wp_send_json_error( array( 'error_code' => 'require_fallback', 'message' => $data['message'] ) );
    289         }
    290         $error_msg = isset( $data['message'] ) ? $data['message'] : __( 'Unknown Server Error.', 'anydress-ai-virtual-try-on' );
    291         wp_send_json_error( $error_msg );
    292     }
    293 
    294     if ( empty( $data['image_url'] ) ) {
    295         wp_send_json_error( __( 'No image data returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
    296     }
     341        // Always relay the server's error code so the JS can style fit-advice
     342        // blocks (size_too_small etc.) differently from real errors.
     343        $error_code = isset( $data['code'] ) ? sanitize_key( $data['code'] ) : 'server_error';
     344        $error_msg  = isset( $data['message'] ) ? $data['message'] : __( 'Unknown Server Error.', 'anydress-ai-virtual-try-on' );
     345        wp_send_json_error( array( 'error_code' => $error_code, 'message' => $error_msg ) );
     346    }
     347
     348    // ASYNC: the server now returns a task_id immediately. The browser polls
     349    // the 'anydress_check_task' action until the image is ready.
     350    if ( empty( $data['task_id'] ) ) {
     351        wp_send_json_error( __( 'No task id returned from AnyDress server.', 'anydress-ai-virtual-try-on' ) );
     352    }
     353
     354    // Count the daily-limit slot NOW (prevents bypassing the limit with parallel
     355    // submissions). If the task terminally fails, the status relay refunds it.
    297356    update_user_meta( $user_id, 'anydress_usage_count', $usage_count + 1 );
    298357
    299     // Success! Send it to the frontend.
    300     wp_send_json_success( array( 'image_url' => $data['image_url'] ) );
     358    wp_send_json_success( array(
     359        'task_id' => sanitize_text_field( $data['task_id'] ),
     360        'status'  => 'pending',
     361    ) );
    301362}
  • anydress-ai-virtual-try-on/trunk/includes/frontend/anydress-cron.php

    r3530062 r3612360  
    5656            // 4. Remove timestamp
    5757            delete_user_meta( $user_id, 'anydress_mannequin_upload_time' );
     58
     59            // 5. Delete saved body measurements (same privacy lifecycle as photos)
     60            delete_user_meta( $user_id, 'anydress_shopper_measurements' );
     61            delete_user_meta( $user_id, 'anydress_shopper_unit' );
    5862        }
    5963    }
  • anydress-ai-virtual-try-on/trunk/includes/frontend/class-anydress-my-account.php

    r3530062 r3612360  
    1616        add_filter( 'woocommerce_account_menu_items', array( $this, 'add_my_account_menu_item' ) );
    1717        add_action( 'template_redirect', array( $this, 'handle_mannequin_deletion' ) );
     18        add_action( 'template_redirect', array( $this, 'handle_measurements_save' ) );
    1819        add_action( 'woocommerce_account_digital-mannequin_endpoint', array( $this, 'my_account_endpoint_content' ) );
     20    }
     21
     22    /**
     23     * All body-measurement fields a shopper can store, across every product type.
     24     * Returns array of payload_key => array( label, unit ).
     25     */
     26    private function get_all_shopper_fields() {
     27        $fields = array();
     28        if ( ! function_exists( 'anydress_size_get_product_types' ) ) {
     29            return $fields;
     30        }
     31        foreach ( anydress_size_get_product_types() as $type ) {
     32            foreach ( $type['shopper_fields'] as $key => $label ) {
     33                if ( ! isset( $fields[ $key ] ) ) {
     34                    $fields[ $key ] = array( 'label' => $label, 'unit' => $type['unit'] );
     35                }
     36            }
     37        }
     38        return $fields;
    1939    }
    2040
    … …  
    84104           
    85105            // 4. Remove timestamp
    86             delete_user_meta( $user_id, 'anydress_mannequin_upload_time' );
     106            delete_user_meta( $user_id, 'anydress_mannequin_upload_time' );
     107
     108            // 5. Delete saved body measurements
     109            delete_user_meta( $user_id, 'anydress_shopper_measurements' );
     110            delete_user_meta( $user_id, 'anydress_shopper_unit' );
    87111
    88112            // Show a success message in WooCommerce
    … …  
    90114           
    91115            // Redirect back to the tab to refresh the page safely
     116            wp_safe_redirect( wc_get_endpoint_url( 'digital-mannequin', '', wc_get_page_permalink( 'myaccount' ) ) );
     117            exit;
     118        }
     119    }
     120
     121    /**
     122     * Handle saving of body measurements from the My Account tab
     123     */
     124    public function handle_measurements_save() {
     125        if ( is_account_page() && isset( $_POST['anydress_save_measurements'] ) && is_user_logged_in() ) {
     126            if ( ! isset( $_POST['anydress_measurements_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['anydress_measurements_nonce'] ) ), 'save_measurements_action' ) ) {
     127                wc_add_notice( __( 'Security check failed. Please try again.', 'anydress-ai-virtual-try-on' ), 'error' );
     128                return;
     129            }
     130
     131            $user_id = get_current_user_id();
     132            $fields  = $this->get_all_shopper_fields();
     133            $unit    = ( isset( $_POST['anydress_unit'] ) && sanitize_text_field( wp_unslash( $_POST['anydress_unit'] ) ) === 'in' ) ? 'in' : 'cm';
     134
     135            $clean = array();
     136            if ( isset( $_POST['anydress_m'] ) && is_array( $_POST['anydress_m'] ) ) {
     137                // Values are numeric strings; validated numerically field-by-field below
     138                $raw = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['anydress_m'] ) );
     139                foreach ( $fields as $key => $def ) {
     140                    if ( isset( $raw[ $key ] ) && $raw[ $key ] !== '' && is_numeric( $raw[ $key ] ) ) {
     141                        $val = (float) $raw[ $key ];
     142                        if ( $val <= 0 ) {
     143                            continue;
     144                        }
     145                        // mm fields (face) are always entered in mm; cm fields follow the chosen unit
     146                        if ( $def['unit'] === 'mm' ) {
     147                            if ( $val <= 400 ) {
     148                                $clean[ $key ] = round( $val, 1 );
     149                            }
     150                        } else {
     151                            $metric = ( $unit === 'in' ) ? round( $val * 2.54, 1 ) : round( $val, 1 );
     152                            if ( $metric <= 350 ) {
     153                                $clean[ $key ] = $metric;
     154                            }
     155                        }
     156                    }
     157                }
     158            }
     159
     160            if ( ! empty( $clean ) ) {
     161                update_user_meta( $user_id, 'anydress_shopper_measurements', $clean );
     162                update_user_meta( $user_id, 'anydress_shopper_unit', $unit );
     163                wc_add_notice( __( 'Your body measurements have been saved.', 'anydress-ai-virtual-try-on' ), 'success' );
     164            } else {
     165                delete_user_meta( $user_id, 'anydress_shopper_measurements' );
     166                delete_user_meta( $user_id, 'anydress_shopper_unit' );
     167                wc_add_notice( __( 'Your body measurements have been removed.', 'anydress-ai-virtual-try-on' ), 'success' );
     168            }
     169
    92170            wp_safe_redirect( wc_get_endpoint_url( 'digital-mannequin', '', wc_get_page_permalink( 'myaccount' ) ) );
    93171            exit;
    … …  
    152230            echo '<a href="' . esc_url( $shop_url ) . '" style="display: inline-block; background: #222; color: #fff; padding: 10px 20px; text-decoration: none; border-radius: 4px;">' . esc_html__( 'Go to Shop', 'anydress-ai-virtual-try-on' ) . '</a>';
    153231            echo '</div>';
    154            
    155             echo '</div>';
    156         }
     232
     233            echo '</div>';
     234        }
     235
     236        $this->render_measurements_section( $user_id );
     237    }
     238
     239    /**
     240     * The "My Body Measurements" edit form inside the My Account tab.
     241     */
     242    private function render_measurements_section( $user_id ) {
     243        $fields = $this->get_all_shopper_fields();
     244        if ( empty( $fields ) ) {
     245            return;
     246        }
     247
     248        $saved = get_user_meta( $user_id, 'anydress_shopper_measurements', true );
     249        if ( ! is_array( $saved ) ) {
     250            $saved = array();
     251        }
     252        $unit = get_user_meta( $user_id, 'anydress_shopper_unit', true );
     253        if ( ! in_array( $unit, array( 'cm', 'in' ), true ) ) {
     254            $unit = 'cm';
     255        }
     256
     257        echo '<h3 style="margin-top: 40px;">' . esc_html__( '📏 My Body Measurements', 'anydress-ai-virtual-try-on' ) . '</h3>';
     258        echo '<p style="color: #666; font-size: 14px;">' . esc_html__( 'Used to make your Virtual Try-On fit as realistic as possible. All fields are optional. Like your mannequin photos, measurements are automatically deleted 7 days after your last mannequin upload.', 'anydress-ai-virtual-try-on' ) . '</p>';
     259
     260        echo '<form method="post" style="background: #fff; padding: 20px; border: 1px solid #e2e2e2; border-radius: 8px;">';
     261        wp_nonce_field( 'save_measurements_action', 'anydress_measurements_nonce' );
     262
     263        // Unit selector
     264        echo '<p style="margin-top: 0;"><strong>' . esc_html__( 'Units:', 'anydress-ai-virtual-try-on' ) . '</strong> ';
     265        echo '<label style="margin-right: 12px;"><input type="radio" name="anydress_unit" value="cm" ' . checked( $unit, 'cm', false ) . ' /> cm</label>';
     266        echo '<label><input type="radio" name="anydress_unit" value="in" ' . checked( $unit, 'in', false ) . ' /> in</label>';
     267        echo ' <small style="color: #888;">' . esc_html__( '(face measurements for eyewear are always in mm)', 'anydress-ai-virtual-try-on' ) . '</small></p>';
     268
     269        echo '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px;">';
     270        foreach ( $fields as $key => $def ) {
     271            $metric_val  = isset( $saved[ $key ] ) ? (float) $saved[ $key ] : '';
     272            $display_val = $metric_val;
     273            $unit_label  = $def['unit'];
     274
     275            if ( $def['unit'] === 'cm' ) {
     276                $unit_label = $unit;
     277                if ( $metric_val !== '' && $unit === 'in' ) {
     278                    $display_val = round( $metric_val / 2.54, 1 );
     279                }
     280            }
     281
     282            echo '<div>';
     283            echo '<label style="display: block; font-size: 13px; font-weight: 600; margin-bottom: 3px;">' . esc_html( $def['label'] ) . ' <small style="font-weight: normal; color: #888;">(' . esc_html( $unit_label ) . ')</small></label>';
     284            echo '<input type="number" step="0.1" min="0" name="anydress_m[' . esc_attr( $key ) . ']" value="' . esc_attr( $display_val ) . '" style="width: 100%; padding: 6px 8px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box;" />';
     285            echo '</div>';
     286        }
     287        echo '</div>';
     288
     289        echo '<div style="margin-top: 15px;">';
     290        echo '<button type="submit" name="anydress_save_measurements" value="1" style="background-color: #222; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: bold;">' . esc_html__( 'Save Measurements', 'anydress-ai-virtual-try-on' ) . '</button>';
     291        echo ' <small style="color: #888; margin-left: 10px;">' . esc_html__( 'Clear all fields and save to delete your measurements.', 'anydress-ai-virtual-try-on' ) . '</small>';
     292        echo '</div>';
     293
     294        echo '</form>';
    157295    }
    158296}
  • anydress-ai-virtual-try-on/trunk/includes/frontend/class-anydress-widget.php

    r3530062 r3612360  
    7070            'productId' => $product_id,
    7171            'nonce'     => wp_create_nonce( 'anydress_frontend_action' ),
     72            'sizing'    => $this->get_sizing_context( $product_id ),
    7273            'i18n'      => array(
    7374                'uploadFace'     => __( 'Please upload your face photo.', 'anydress-ai-virtual-try-on' ),
    7475                'faceCompressed' => __( '⚠️ Your face photo could not be compressed enough. Please try a different image.', 'anydress-ai-virtual-try-on' ),
    7576                'bodyCompressed' => __( '⚠️ Your body photo could not be compressed enough. Please try a different image.', 'anydress-ai-virtual-try-on' ),
    76                 'errorProcess'   => __( 'Error processing images. Please try different photos.', 'anydress-ai-virtual-try-on' )
     77                'errorProcess'   => __( 'Error processing images. Please try different photos.', 'anydress-ai-virtual-try-on' ),
     78                'fitWarning'     => __( 'Fit note:', 'anydress-ai-virtual-try-on' )
    7779            )
    7880        ));
     81    }
     82
     83    /**
     84     * Build the sizing context for the frontend JS.
     85     * Returns null when the product has no size chart assigned (plugin behaves exactly as before).
     86     * NOTE: garment measurements themselves never leave the server — only size labels go to the browser.
     87     */
     88    private function get_sizing_context( $product_id ) {
     89        if ( ! function_exists( 'anydress_size_get_product_chart' ) ) {
     90            return null;
     91        }
     92
     93        $chart = anydress_size_get_product_chart( $product_id );
     94        if ( ! $chart ) {
     95            return null;
     96        }
     97
     98        $type_def = anydress_size_get_type( $chart['product_type'] );
     99        if ( ! $type_def ) {
     100            return null;
     101        }
     102
     103        $size_labels = array();
     104        foreach ( $chart['rows'] as $row ) {
     105            $size_labels[] = $row['label'];
     106        }
     107
     108        // Locate the size attribute so JS can sync with the variation dropdown
     109        $size_attr_field = '';
     110        $product = wc_get_product( $product_id );
     111        $size_attr = anydress_size_get_product_size_attribute( $product );
     112        if ( $size_attr ) {
     113            $size_attr_field = 'attribute_' . sanitize_title( $size_attr['attribute_name'] );
     114        }
     115
     116        // Saved shopper measurements (stored in metric)
     117        $user_id = get_current_user_id();
     118        $saved   = $user_id ? get_user_meta( $user_id, 'anydress_shopper_measurements', true ) : array();
     119        if ( ! is_array( $saved ) ) {
     120            $saved = array();
     121        }
     122        $saved_unit = $user_id ? get_user_meta( $user_id, 'anydress_shopper_unit', true ) : '';
     123        if ( ! in_array( $saved_unit, array( 'cm', 'in' ), true ) ) {
     124            $saved_unit = 'cm';
     125        }
     126
     127        return array(
     128            'productType'    => $chart['product_type'],
     129            'metricUnit'     => $type_def['unit'],          // what the server expects (cm or mm)
     130            'allowImperial'  => (bool) $type_def['allow_imperial'],
     131            'preferredUnit'  => $saved_unit,
     132            'sizeLabels'     => $size_labels,
     133            'sizeAttrField'  => $size_attr_field,
     134            'shopperFields'  => $type_def['shopper_fields'],
     135            'saved'          => (object) $saved,
     136        );
    79137    }
    80138
    … …  
    193251        echo '        </div>';
    194252
     253        // --- SIZING & FIT SECTION (only when the product has a size chart) ---
     254        // Note: get_queried_object_id() is reliable in wp_footer, unlike get_the_ID() after custom loops
     255        $sizing = $this->get_sizing_context( get_queried_object_id() );
     256        if ( $sizing ) {
     257            $this->render_fit_section( $sizing );
     258        }
     259
    195260        echo '        <div id="anydress-result-area"></div>';
    196261
    … …  
    198263        echo '</div>';
    199264    }
     265
     266    /**
     267     * Render the optional "Get an accurate fit" block inside the modal.
     268     * Works for both the saved-mannequin flow and the new-upload flow.
     269     */
     270    private function render_fit_section( $sizing ) {
     271        $is_mm = ( $sizing['metricUnit'] === 'mm' );
     272
     273        echo '<div id="anydress-fit-section" class="anydress-fit-section">';
     274        echo '  <details id="anydress-fit-details">';
     275        echo '      <summary>' . esc_html__( '📏 Get an accurate fit (optional)', 'anydress-ai-virtual-try-on' ) . '</summary>';
     276        echo '      <div class="anydress-fit-body">';
     277
     278        // 1. Size picker (synced with the variation dropdown when one exists)
     279        echo '          <div class="anydress-fit-row">';
     280        echo '              <label for="anydress-fit-size">' . esc_html__( 'Size you\'re trying on', 'anydress-ai-virtual-try-on' ) . '</label>';
     281        echo '              <select id="anydress-fit-size">';
     282        echo '                  <option value="">' . esc_html__( '— Select size —', 'anydress-ai-virtual-try-on' ) . '</option>';
     283        foreach ( $sizing['sizeLabels'] as $label ) {
     284            echo '              <option value="' . esc_attr( $label ) . '">' . esc_html( $label ) . '</option>';
     285        }
     286        echo '              </select>';
     287        echo '          </div>';
     288
     289        // 2. Unit toggle (hidden for mm-based products like eyewear)
     290        if ( ! $is_mm && $sizing['allowImperial'] ) {
     291            echo '      <div class="anydress-fit-row anydress-fit-unit-row">';
     292            echo '          <label>' . esc_html__( 'Units', 'anydress-ai-virtual-try-on' ) . '</label>';
     293            echo '          <span>';
     294            echo '              <label class="anydress-fit-unit-opt"><input type="radio" name="anydress_fit_unit" value="cm" ' . checked( $sizing['preferredUnit'], 'cm', false ) . ' /> cm</label>';
     295            echo '              <label class="anydress-fit-unit-opt"><input type="radio" name="anydress_fit_unit" value="in" ' . checked( $sizing['preferredUnit'], 'in', false ) . ' /> in</label>';
     296            echo '          </span>';
     297            echo '      </div>';
     298        }
     299
     300        // 3. Measurement inputs (values injected by JS from saved data, converted to the active unit)
     301        echo '          <p class="anydress-fit-hint">' . esc_html__( 'Your measurements are optional — the more you fill in, the more realistic the generated fit. They are saved with your digital mannequin and automatically deleted after 7 days.', 'anydress-ai-virtual-try-on' ) . '</p>';
     302        echo '          <div class="anydress-fit-fields">';
     303        foreach ( $sizing['shopperFields'] as $key => $label ) {
     304            $unit_suffix = $is_mm ? 'mm' : ''; // body-field suffix is drawn by JS (follows the toggle)
     305            echo '      <div class="anydress-fit-field">';
     306            echo '          <label for="anydress-fit-' . esc_attr( $key ) . '">' . esc_html( $label ) . ( $unit_suffix ? ' <small>(' . esc_html( $unit_suffix ) . ')</small>' : ' <small class="anydress-fit-unit-label"></small>' );
     307            echo '          </label>';
     308            echo '          <input type="number" step="0.1" min="0" inputmode="decimal" id="anydress-fit-' . esc_attr( $key ) . '" class="anydress-fit-input" data-key="' . esc_attr( $key ) . '" data-fixed-unit="' . esc_attr( $is_mm ? 'mm' : '' ) . '" />';
     309            echo '      </div>';
     310        }
     311        echo '          </div>';
     312
     313        echo '      </div>';
     314        echo '  </details>';
     315        echo '</div>';
     316    }
    200317}
    201318
  • anydress-ai-virtual-try-on/trunk/readme.txt

    r3530142 r3612360  
    1 === AnyDress AI Virtual Try-On ===
     1=== AnyDress – AI Virtual Try-On for WooCommerce & AI Product Photos ===
    22Contributors: anydress
    3 Tags: woocommerce, virtual try on, ai, fashion, ecommerce
     3Tags: virtual try on, woocommerce, ai, product images, clothing
    44Requires at least: 5.8
    5 Tested up to: 6.9
    6 Stable tag: 1.0.0
     5Tested up to: 7.0
     6Stable tag: 1.3.1
    77Requires PHP: 7.4
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 Elevate your digital boutique with seamless AI virtual try-on and instant studio-quality catalog photoshoots.
     11AI virtual try-on for WooCommerce: shoppers try on clothes on their own photo, and you generate studio-quality product images without models.
    1212
    1313== Description ==
    1414
    15 AnyDress AI Virtual Try-On revolutionizes your WooCommerce store by allowing shoppers to visually "try on" your clothing using artificial intelligence. Increase conversion rates, boost buyer confidence, and reduce return rates by letting customers see exactly how your garments will look on their own bodies.
     15**AnyDress adds an AI virtual fitting room to your WooCommerce store — and an AI photo studio to your dashboard.** Shoppers can virtually try on clothes using their own photos before they buy, and store owners can generate professional product images without models, photographers, or physical photoshoots.
    1616
    17 Additionally, store owners can use the Virtual Photoshoot dashboard to generate professional, studio-quality catalog images of their garments using AI models or ghost mannequins, saving thousands of dollars on physical photography.
     17One plugin, two revenue levers: higher conversions from confident shoppers, and lower costs from AI-generated catalog photography.
    1818
    19 = Features =
    20 *   **Shopper Virtual Try-On:** Adds a customizable "Try It On" button to WooCommerce product pages.
    21 *   **Digital Mannequin:** Logged-in shoppers can securely save their face and body photos for 1-click try-ons across your whole store.
    22 *   **Admin Virtual Photoshoot:** Instantly generate new product catalog images using WooCommerce products or raw garment uploads.
    23 *   **AI Models & Ghost Mannequins:** Choose to use the original model, upload your own, generate a random AI model, or create invisible ghost mannequins.
    24 *   **Custom Backgrounds:** Describe any scene or background to place your models in.
    25 *   **Magic Edit:** Alter the pose, aspect ratio, resolution, and photography style of your generated images.
    26 *   **Usage Controls:** Set daily try-on limits to protect your API budget.
    27 *   **Privacy-First Design:** Built-in legal consent checkboxes for both shoppers and admins, ensuring no data is sent to the AI engine without explicit user agreement.
     19= A virtual fitting room for your shoppers =
     20
     21A customizable **"Try It On" button** appears on your product pages, right next to Add to Cart. Shoppers upload a photo (or reuse their saved Digital Mannequin) and see exactly how the garment looks on their own body — not on a model who looks nothing like them.
     22
     23* **1-click try-ons across your whole store** — logged-in shoppers securely save face and body photos once, then try on anything.
     24* **True-to-size fit** — pair try-ons with real garment measurements so shoppers see (or get warned about) sizes that run tight or loose. Fewer sizing mistakes, fewer returns, less bracketing.
     25* **Works with your theme** — hooks into standard WooCommerce templates automatically.
     26
     27= An AI photo studio for store owners =
     28
     29The **Virtual Photoshoot dashboard** turns a flat lay, hanger shot, or plain packshot into studio-quality on-model imagery in minutes:
     30
     31* Generate catalog images from your existing WooCommerce product photos or raw garment uploads.
     32* Choose an **AI model**, upload your own model, or create clean **ghost mannequin** shots.
     33* Describe any **custom background** or scene in plain words.
     34* **Magic Edit** — change pose, aspect ratio, resolution, and photography style after generation.
     35
     36No studio rental, no model fees, no reshoots when your catalog changes.
     37
     38= Built-in size charts =
     39
     40Create reusable size charts (tops, bottoms, dresses, outerwear, swimwear, eyewear, headwear) with real garment measurements per size. Assign them to products in bulk, override values per product, and let shoppers optionally add their own measurements (cm or inches) for a realistic fit.
     41
     42= Privacy-first by design =
     43
     44* Explicit consent checkboxes for both shoppers and admins — no data reaches the AI engine without agreement.
     45* Shopper photos live on **your own server**, auto-deleted after 7 days; shoppers can delete their data anytime from My Account.
     46* Images are processed in memory and discarded by the API immediately after generation — never used to train AI models.
     47
     48= Perfect for =
     49
     50* [Apparel & clothing stores](https://anydress.ai/use-cases/apparel/)
     51* [Eyewear & glasses](https://anydress.ai/use-cases/eyewear/)
     52* [Baby & kids clothes](https://anydress.ai/use-cases/baby-clothes/)
     53* [Jewelry](https://anydress.ai/use-cases/jewelry/)
     54* [Other use cases](https://anydress.ai/use-cases/)
     55
     56= All features =
     57
     58* **Shopper Virtual Try-On:** customizable "Try It On" button on WooCommerce product pages.
     59* **Digital Mannequin:** shoppers securely save photos for 1-click try-ons store-wide.
     60* **Admin Virtual Photoshoot:** generate product catalog images from products or raw garment uploads.
     61* **AI Models & Ghost Mannequins:** original model, your own model, a generated AI model, or invisible ghost mannequins.
     62* **Custom Backgrounds:** describe any scene to place your models in.
     63* **Magic Edit:** alter pose, aspect ratio, resolution, and photography style.
     64* **Size Charts & True-to-Size Fit:** reusable charts with per-size garment measurements, bulk assignment, per-product overrides.
     65* **Shopper Measurements (optional):** private body measurements for realistic fit rendering and sizing advice.
     66* **Usage Controls:** daily try-on limits to protect your API budget.
     67* **Privacy-First Design:** built-in legal consent flows for shoppers and admins.
     68
     69= Learn more =
     70
     71* [How AI virtual try-on works in WooCommerce](https://anydress.ai/virtual-try-on-for-woocommerce-guide/)
     72* [Creating product images without models](https://anydress.ai/ai-clothing-photography-without-a-model/)
     73* [Reducing clothing return rates](https://anydress.ai/reduce-clothing-return-rates-woocommerce/)
     74* [Plans and pricing](https://anydress.ai/#pricing)
    2875
    2976== External Services ==
    … …  
    3279
    3380* What the service is used for: The AnyDress AI API provides the artificial intelligence engine required to "clothe" a digital mannequin with a product image and to generate studio-quality catalog photoshoots.
    34 * What data is sent and when: 
     81* What data is sent and when:
    3582    1. Shopper Try-On: Only after a logged-in user gives explicit consent via a checkbox, their uploaded photos (Face/Body) and the WooCommerce product image are converted to Base64 and sent to the AnyDress AI endpoint.
    36     2. Admin Photoshoot: Only after the administrator confirms rights to the images, the selected garment and model photos are sent to the API.
    37     3. Verification: The store domain and license key are sent to verify subscription status and usage limits.
     83    2. Optional Fit Data: If the shopper voluntarily enters body measurements, those numbers (together with the store's garment measurements for the selected size) are sent alongside the try-on request to render a realistic fit. Measurements are never required.
     84    3. Admin Photoshoot: Only after the administrator confirms rights to the images, the selected garment and model photos are sent to the API.
     85    4. Verification & Task Status: The store domain, license key, and generation task IDs are sent to verify subscription status, usage limits, and to retrieve finished images.
    3886* Data Handling & Privacy: Images are processed in secure server memory to generate the result and are immediately discarded by the API upon completion. User data is confidential and never used to train AI models.
    3987*   **Terms & Privacy Links:**
    … …  
    53101== Frequently Asked Questions ==
    54102
     103= How does the AI virtual try-on work for shoppers? =
     104A "Try It On" button appears on your product pages. The shopper uploads a photo of themselves (or reuses their saved Digital Mannequin), and the AI renders the garment on their own body in seconds — including realistic fit if measurements and size charts are set up.
     105
     106= Can I really create product images without hiring models or a photographer? =
     107Yes. The Virtual Photoshoot dashboard turns a flat lay, hanger shot, ghost mannequin shot, or plain packshot into on-model, studio-quality catalog images. You choose the model, background, pose, and style.
     108
     109= Does virtual try-on reduce return rates? =
     110Sizing and "it looked different online" are the top reasons clothing gets returned. Letting shoppers see garments on their own body — with optional true-to-size fit warnings — addresses both causes directly. See our [guide to reducing WooCommerce return rates](https://anydress.ai/reduce-clothing-return-rates-woocommerce/).
     111
    55112= Do I need an AnyDress AI account to use this plugin? =
    56 Yes, you need an active API License Key from AnyDress AI to connect your WooCommerce store to the AI generation engine.
     113Yes, you need an active API License Key from AnyDress AI to connect your WooCommerce store to the AI generation engine. Plans are listed at https://anydress.ai/#pricing.
     114
     115= Does it work for eyewear, accessories, or kids' clothes? =
     116Yes — size charts cover tops, bottoms, dresses, outerwear, swimwear, eyewear, and headwear, and stores use AnyDress for apparel, glasses, baby and kids' clothes, and jewelry.
    57117
    58118= Where are shopper photos saved? =
    59119Shopper photos are securely saved in a hidden, local folder on your own WordPress server (`wp-content/uploads/anydress-mannequins/`). To protect privacy and save disk space, a cron job automatically deletes shopper photos 7 days after upload. Shoppers can also manually delete their data at any time via the "Digital Mannequin" tab in their My Account page.
     120
     121= Are shopper body measurements required? Where are they stored? =
     122No — measurements are entirely optional; without them the try-on works exactly as before. If provided, they are stored on your own WordPress server as user meta, editable and deletable by the shopper in My Account, and automatically removed by the same 7-day cleanup that deletes mannequin photos (and on plugin uninstall).
     123
     124= How do I set up sizing for my products? =
     125Go to **AnyDress AI → Size Charts**, create a chart with your garment measurements per size, and assign it to products either from the chart itself or in the product editor under **Product data → AnyDress Sizing** (where you can also override individual values per product).
    60126
    61127= Does this work with any theme? =
    … …  
    64130== Screenshots ==
    65131
    66 1. The Virtual Try-On button on a WooCommerce product page.
    67 2. The shopper upload modal.
    68 3. The shopper Virtual Try-On result.
    69 4. The Admin Virtual Photoshoot dashboard.
     1321. The "Try It On" button on a WooCommerce product page — shoppers start a virtual try-on next to Add to Cart.
     1332. The shopper upload modal with consent and Digital Mannequin options.
     1343. A finished virtual try-on: the garment rendered on the shopper's own photo.
     1354. The Admin Virtual Photoshoot dashboard — AI product images without models.
    701365. AnyDress plugin settings and API dashboard.
    71137
    72138== Changelog ==
    73139
     140= 1.3.1 =
     141* Hardened input sanitization and declared compatibility with WordPress 7.0 (Plugin Check compliance).
     142
     143= 1.3.0 =
     144* New settings: choose the Virtual Try-On aspect ratio (2:3, 3:4, 4:5) and resolution (1K or 2K) so every shopper result has a consistent, predictable format.
     145* Friendlier sizing feedback: when a selected size doesn't fit the shopper's measurements, the try-on now shows a helpful sizing-advice notice (instead of an error) and opens the size picker so they can switch and retry.
     146
     147= 1.2.0 =
     148* Faster, more reliable generations: try-ons are now processed asynchronously — the page shows progress and delivers the image the moment it is ready, with no long-hanging requests.
     149* Improved reliability on slow hosts: generation no longer depends on a single long HTTP request.
     150
     151= 1.1.1 =
     152* Size Charts: assign a chart to many products at once with a searchable product picker, and duplicate charts for product models with different measurements.
     153
     154= 1.1.0 =
     155* New Size Charts system: reusable per-size garment measurements for tops, bottoms, dresses, outerwear, swimwear, eyewear, and headwear.
     156* Optional shopper body measurements (cm/inches) for a true-to-size virtual try-on, with a store setting to block or realistically render ill-fitting sizes.
     157* Per-product measurement overrides and automatic matching with WooCommerce size attributes.
     158* Fixed an issue where the face photo was uploaded twice, which could exceed server upload limits.
     159
    74160= 1.0.0 =
    75161* Initial release.
  • anydress-ai-virtual-try-on/trunk/uninstall.php

    r3530062 r3612360  
    2323    'anydress_checkbox_tick_color',
    2424    'anydress_privacy_page_id', // Legacy setting
    25     'anydress_plugin_version'
     25    'anydress_plugin_version',
     26    'anydress_privacy_policy_url',
     27    'anydress_fit_mode',
     28    'anydress_size_charts'
    2629);
    2730
    … …  
    4245    'anydress_mannequin_upload_time',
    4346    'anydress_usage_date',
    44     'anydress_usage_count'
     47    'anydress_usage_count',
     48    'anydress_shopper_measurements',
     49    'anydress_shopper_unit'
    4550);
    4651
    … …  
    5055    delete_metadata( 'user', 0, $anydress_meta_key, '', true );
    5156}
     57
     58// 2b. Delete product sizing meta for ALL products
     59delete_metadata( 'post', 0, '_anydress_size_chart_id', '', true );
     60delete_metadata( 'post', 0, '_anydress_size_overrides', '', true );
    5261
    5362// 3. Delete the custom mannequins folder and all images inside it
Note: See TracChangeset for help on using the changeset viewer.