_x( 'Model Textures', 'post type general name', '3d-product-customizer' ), 'singular_name' => _x( 'Model Texture', 'post type singular name', '3d-product-customizer' ), 'menu_name' => _x( 'Model Textures', 'admin menu', '3d-product-customizer' ), 'new_item' => __( 'New Model Texture', '3d-product-customizer' ), 'add_new' => __( 'Add New', '3d-product-customizer' ), 'add_new_item' => __( 'Add New Model Texture', '3d-product-customizer' ), 'edit_item' => __( 'Edit Model Texture', '3d-product-customizer' ), 'view_item' => __( 'View Model Texture', '3d-product-customizer' ), 'all_items' => __( 'All Model Textures', '3d-product-customizer' ), 'search_items' => __( 'Search Model Textures', '3d-product-customizer' ), 'parent_item_colon' => __( 'Parent Model Textures:', '3d-product-customizer' ), 'not_found' => __( 'No model textures found.', '3d-product-customizer' ), 'not_found_in_trash' => __( 'No model textures found in Trash.', '3d-product-customizer' ), ); $args = array( 'labels' => $labels, 'public' => false, 'publicly_queryable' => false, 'show_ui' => true, 'show_in_menu' => 'dprcu-settings', 'query_var' => false, 'rewrite' => false, 'capability_type' => 'post', 'has_archive' => false, 'hierarchical' => false, 'menu_position' => 20, 'menu_icon' => 'dashicons-format-image', 'supports' => array('title', 'thumbnail'), 'show_in_rest' => true, ); register_post_type( 'dprcu_model_texture', $args ); } add_action( 'init', 'dprcu_textures_cpt_init' ); /** * Register the 'Texture Category' taxonomy for Model Textures. */ function dprcu_textures_taxonomy_init() { $labels = array( 'name' => _x( 'Texture Categories', 'taxonomy general name', '3d-product-customizer' ), 'singular_name' => _x( 'Texture Category', 'taxonomy singular name', '3d-product-customizer' ), 'search_items' => __( 'Search Texture Categories', '3d-product-customizer' ), 'all_items' => __( 'All Texture Categories', '3d-product-customizer' ), 'parent_item' => __( 'Parent Category', '3d-product-customizer' ), 'parent_item_colon' => __( 'Parent Category:', '3d-product-customizer' ), 'edit_item' => __( 'Edit Texture Category', '3d-product-customizer' ), 'update_item' => __( 'Update Texture Category', '3d-product-customizer' ), 'add_new_item' => __( 'Add New Texture Category', '3d-product-customizer' ), 'new_item_name' => __( 'New Texture Category Name', '3d-product-customizer' ), 'menu_name' => __( 'Categories', '3d-product-customizer' ), ); register_taxonomy( 'dprcu_texture_category', 'dprcu_model_texture', array( 'labels' => $labels, 'hierarchical' => true, 'show_ui' => true, 'show_in_menu' => true, 'show_in_rest' => true, 'show_admin_column' => true, 'rewrite' => false, 'query_var' => false, ) ); } add_action( 'init', 'dprcu_textures_taxonomy_init' ); /** * Add the Texture Categories submenu under the plugin's custom menu. * * WordPress only auto-adds taxonomy submenus for CPTs with their own * top-level menu entry. Since dprcu_model_texture uses show_in_menu pointing * to 'dprcu-settings', the submenu must be registered manually. */ function dprcu_textures_taxonomy_submenu() { add_submenu_page( 'dprcu-settings', __( 'Texture Categories', '3d-product-customizer' ), __( 'Texture Categories', '3d-product-customizer' ), 'manage_options', 'edit-tags.php?taxonomy=dprcu_texture_category&post_type=dprcu_model_texture' ); } add_action( 'admin_menu', 'dprcu_textures_taxonomy_submenu' ); /** * Highlight the parent and submenu items when viewing dprcu_texture_category taxonomy. * * WordPress doesn't auto-highlight the parent menu for taxonomy edit pages, * so we manually set both parent_file and submenu_file to ensure proper menu highlighting. */ function dprcu_highlight_taxonomy_menu( $parent_file ) { global $pagenow, $submenu_file; if ( 'edit-tags.php' === $pagenow ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only context check for menu highlighting, no form processing $taxonomy = ( isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only context check for menu highlighting, no form processing $post_type = ( isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : '' ); if ( 'dprcu_texture_category' === $taxonomy && 'dprcu_model_texture' === $post_type ) { $parent_file = 'dprcu-settings'; $submenu_file = 'edit-tags.php?taxonomy=dprcu_texture_category&post_type=dprcu_model_texture'; } } return $parent_file; } add_filter( 'parent_file', 'dprcu_highlight_taxonomy_menu' ); /** * Add a Texture Category filter dropdown to the dprcu_model_texture list table. */ function dprcu_textures_category_filter_dropdown( $post_type ) { if ( 'dprcu_model_texture' !== $post_type ) { return; } $terms = get_terms( array( 'taxonomy' => 'dprcu_texture_category', 'hide_empty' => false, ) ); if ( empty( $terms ) || is_wp_error( $terms ) ) { return; } // Check nonce for security $selected = ''; if ( isset( $_GET['dprcu_texture_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['dprcu_texture_nonce'] ) ), 'dprcu_texture_filter' ) ) { $selected = ( isset( $_GET['dprcu_texture_category'] ) ? sanitize_key( wp_unslash( $_GET['dprcu_texture_category'] ) ) : '' ); } ?> is_main_query() ) { return; } if ( 'dprcu_model_texture' !== $query->get( 'post_type' ) ) { return; } // Check nonce for security if ( !isset( $_GET['dprcu_texture_nonce'] ) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['dprcu_texture_nonce'] ) ), 'dprcu_texture_filter' ) ) { return; } $cat = ( isset( $_GET['dprcu_texture_category'] ) ? sanitize_key( wp_unslash( $_GET['dprcu_texture_category'] ) ) : '' ); if ( empty( $cat ) ) { return; } $query->set( 'tax_query', array(array( 'taxonomy' => 'dprcu_texture_category', 'field' => 'slug', 'terms' => $cat, )) ); } add_action( 'parse_query', 'dprcu_textures_category_filter_query' ); /** * Add the custom metabox for optional texture maps. */ function dprcu_textures_add_metaboxes() { add_meta_box( 'dprcu_texture_maps', 'Optional Texture Maps', 'dprcu_textures_metabox_html', 'dprcu_model_texture', 'normal', 'default' ); if ( dprcu_fs()?->can_use_premium_code__premium_only() ) { add_meta_box( 'dprcu_texture_preview', __( 'Material Preview', '3d-product-customizer' ), 'dprcu_textures_preview_metabox_html__premium_only', 'dprcu_model_texture', 'side', 'high' ); } else { add_meta_box( 'dprcu_texture_preview', __( 'Material Preview', '3d-product-customizer' ), 'dprcu_textures_preview_metabox_html_free', 'dprcu_model_texture', 'side', 'high' ); } add_meta_box( 'dprcu_texture_color_swatch', __( 'Color Swatch', '3d-product-customizer' ), 'dprcu_textures_color_swatch_metabox_html', 'dprcu_model_texture', 'side', 'default' ); } add_action( 'add_meta_boxes', 'dprcu_textures_add_metaboxes' ); /** * Render the Material Preview metabox (free — upsell). */ function dprcu_textures_preview_metabox_html_free() { $upgrade_url = dprcu_fs()->get_upgrade_url(); ?> ID, '_dprcu_texture_color', true ) ?: '' ); ?>

ID, '_dprcu_texture_roughness_id', true ); $normal_id = get_post_meta( $post->ID, '_dprcu_texture_normal_id', true ); $roughness_url = ( $roughness_id ? wp_get_attachment_image_url( $roughness_id, 'thumbnail' ) : '' ); $normal_url = ( $normal_id ? wp_get_attachment_image_url( $normal_id, 'thumbnail' ) : '' ); // Slider values $roughness = get_post_meta( $post->ID, '_dprcu_texture_roughness', true ); if ( '' === $roughness ) { $roughness = '0.5'; // Default } $metalness = get_post_meta( $post->ID, '_dprcu_texture_metalness', true ); if ( '' === $metalness ) { $metalness = '0.0'; // Default Plastic } $env_intensity = get_post_meta( $post->ID, '_dprcu_texture_env_intensity', true ); if ( '' === $env_intensity ) { $env_intensity = '1.0'; // Default } $disp_scale = get_post_meta( $post->ID, '_dprcu_texture_disp_scale', true ); if ( '' === $disp_scale ) { $disp_scale = '0.01'; // Default } ?>



can_use_premium_code__premium_only() ) { ?> $roughness_id, 'roughness_url' => $roughness_url, 'normal_id' => $normal_id, 'normal_url' => $normal_url, 'env_intensity' => $env_intensity, 'disp_scale' => $disp_scale, ); dprcu_textures_render_advanced_material__premium_only( $post, $data ); ?> __( 'Advanced Material Controls', '3d-product-customizer' ), 'free_features' => array( __( 'Upload 3D model (.glb / .gltf)', '3d-product-customizer' ), __( 'Base color texture per material', '3d-product-customizer' ), __( 'Roughness & metalness value sliders', '3d-product-customizer' ), __( 'Customization steps (1 texture per option)', '3d-product-customizer' ), __( 'Viewer Only Mode (model in product gallery)', '3d-product-customizer' ), __( 'Camera, background & zoom settings', '3d-product-customizer' ), __( 'HDR environment map support', '3d-product-customizer' ) ), 'premium_features' => array( __( 'Multiple textures per option', '3d-product-customizer' ), __( 'Bulk Add Materials per step', '3d-product-customizer' ), __( 'Show / Hide mesh controls per option', '3d-product-customizer' ), __( 'Roughness Map texture', '3d-product-customizer' ), __( 'Normal Map texture', '3d-product-customizer' ), __( 'Ambient Occlusion (AO) Map', '3d-product-customizer' ), __( 'Metalness Map texture', '3d-product-customizer' ), __( 'Displacement Map texture', '3d-product-customizer' ), __( 'Reflection Intensity control', '3d-product-customizer' ), __( 'Displacement Scale control', '3d-product-customizer' ) ), ) ); } ?>
post_parent !== $post_id ) { // Update the attachment's parent wp_update_post( array( 'ID' => $attachment_id, 'post_parent' => $post_id, ) ); } } }; // ------------------------------------ // Save Roughness ID and attach it. // absint() is correct here — attachment IDs are always positive integers. if ( isset( $_POST['dprcu_texture_roughness_id'] ) ) { $roughness_id = absint( wp_unslash( $_POST['dprcu_texture_roughness_id'] ) ); update_post_meta( $post_id, '_dprcu_texture_roughness_id', $roughness_id ); $attach_image( $roughness_id, $post_id ); } // Save Normal ID and attach it. if ( isset( $_POST['dprcu_texture_normal_id'] ) ) { $normal_id = absint( wp_unslash( $_POST['dprcu_texture_normal_id'] ) ); update_post_meta( $post_id, '_dprcu_texture_normal_id', $normal_id ); $attach_image( $normal_id, $post_id ); } // Save AO ID and attach it. if ( isset( $_POST['dprcu_texture_ao_id'] ) ) { $ao_id = absint( wp_unslash( $_POST['dprcu_texture_ao_id'] ) ); update_post_meta( $post_id, '_dprcu_texture_ao_id', $ao_id ); $attach_image( $ao_id, $post_id ); } // Save Metalness ID and attach it. if ( isset( $_POST['dprcu_texture_metalness_id'] ) ) { $metalness_id = absint( wp_unslash( $_POST['dprcu_texture_metalness_id'] ) ); update_post_meta( $post_id, '_dprcu_texture_metalness_id', $metalness_id ); $attach_image( $metalness_id, $post_id ); } // Save Displacement ID and attach it. if ( isset( $_POST['dprcu_texture_displacement_id'] ) ) { $displacement_id = absint( wp_unslash( $_POST['dprcu_texture_displacement_id'] ) ); update_post_meta( $post_id, '_dprcu_texture_displacement_id', $displacement_id ); $attach_image( $displacement_id, $post_id ); } if ( isset( $_POST['dprcu_texture_roughness'] ) ) { update_post_meta( $post_id, '_dprcu_texture_roughness', floatval( wp_unslash( $_POST['dprcu_texture_roughness'] ) ) ); } if ( isset( $_POST['dprcu_texture_metalness'] ) ) { update_post_meta( $post_id, '_dprcu_texture_metalness', floatval( wp_unslash( $_POST['dprcu_texture_metalness'] ) ) ); } if ( isset( $_POST['dprcu_texture_env_intensity'] ) ) { update_post_meta( $post_id, '_dprcu_texture_env_intensity', floatval( wp_unslash( $_POST['dprcu_texture_env_intensity'] ) ) ); } if ( isset( $_POST['dprcu_texture_disp_scale'] ) ) { update_post_meta( $post_id, '_dprcu_texture_disp_scale', floatval( wp_unslash( $_POST['dprcu_texture_disp_scale'] ) ) ); } if ( isset( $_POST['dprcu_texture_color'] ) ) { $color = sanitize_hex_color( wp_unslash( $_POST['dprcu_texture_color'] ) ); update_post_meta( $post_id, '_dprcu_texture_color', ( $color ? $color : '' ) ); } } add_action( 'save_post', 'dprcu_textures_save_postdata' ); /** * Add admin columns for 'baseColor' and re-order. */ function dprcu_admin_columns( $columns ) { $new_columns = array(); $new_columns['cb'] = $columns['cb']; // Checkbox $new_columns['base_color'] = __( 'Base Color', '3d-product-customizer' ); // Custom column $new_columns['title'] = $columns['title']; // Title $new_columns['date'] = $columns['date']; // Date return $new_columns; } add_filter( 'manage_dprcu_model_texture_posts_columns', 'dprcu_admin_columns' ); /** * Display content for the custom 'base_color' column. */ function dprcu_custom_column_content( $column, $post_id ) { if ( 'base_color' === $column ) { if ( has_post_thumbnail( $post_id ) ) { echo get_the_post_thumbnail( $post_id, array(80, 80) ); } else { $color = get_post_meta( $post_id, '_dprcu_texture_color', true ); if ( $color ) { echo '
' . esc_html__( 'HEX', '3d-product-customizer' ) . '
'; } else { echo esc_html__( '—', '3d-product-customizer' ); } } } } add_action( 'manage_dprcu_model_texture_posts_custom_column', 'dprcu_custom_column_content', 10, 2 ); /** * Enqueue admin CSS for the grid view. */ function dprcu_admin_grid_styles( $hook ) { global $post_type; if ( 'edit.php' === $hook && 'dprcu_model_texture' === $post_type || 'dprcu_model_texture' === get_post_type() && in_array( $hook, array('post-new.php', 'post.php'), true ) ) { wp_enqueue_style( 'dprcu-textures-grid-view', plugin_dir_url( __FILE__ ) . 'admin/css/textures-grid.css', array(), '1.0' ); } } add_action( 'admin_enqueue_scripts', 'dprcu_admin_grid_styles' ); /** * Returns attachment IDs that belong to dprcu_model_texture posts and * should be hidden from the main Media Library. * * Covers two cases: * - Thumbnail (featured image) set from an existing media file — stored as * _thumbnail_id meta, post_parent stays 0, so post_parent__not_in misses them. * - Files uploaded directly inside the texture post — post_parent is the texture * post ID, caught via a separate attachment query. */ function dprcu_get_texture_attachment_ids() : array { static $ids = null; if ( $ids !== null ) { return $ids; } $texture_post_ids = get_posts( [ 'post_type' => 'dprcu_model_texture', 'posts_per_page' => -1, 'fields' => 'ids', 'post_status' => 'any', ] ); if ( empty( $texture_post_ids ) ) { return $ids = []; } // Thumbnail IDs (featured images assigned from the media library). $thumb_ids = array_filter( array_map( fn( $id ) => (int) get_post_thumbnail_id( $id ), $texture_post_ids ) ); // Attachments uploaded directly to a texture post. $parented_ids = get_posts( [ 'post_type' => 'attachment', 'post_parent__in' => $texture_post_ids, 'posts_per_page' => -1, 'fields' => 'ids', 'post_status' => 'any', ] ); return $ids = array_values( array_unique( [...$thumb_ids, ...$parented_ids] ) ); } // Add a "Texture Images" tab to the media library filter bar (list view). add_filter( 'views_upload', function ( $views ) { $showing = isset( $_GET['dprcu_textures'] ); $url = ( $showing ? remove_query_arg( 'dprcu_textures', admin_url( 'upload.php' ) ) : add_query_arg( 'dprcu_textures', '1', admin_url( 'upload.php' ) ) ); $label = ( $showing ? __( 'Texture Images (showing)', '3d-product-customizer' ) : __( 'Texture Images', '3d-product-customizer' ) ); $views['dprcu_textures'] = sprintf( '%s', esc_url( $url ), ( $showing ? ' class="current" aria-current="page"' : '' ), esc_html( $label ) ); return $views; } ); function dprcu_hide_from_media_library( $query ) { if ( !is_admin() || !function_exists( 'get_current_screen' ) ) { return; } $screen = get_current_screen(); if ( !$screen || 'upload' !== $screen->id || !$query->is_main_query() ) { return; } // "Texture Images" tab active — show only texture attachments. if ( isset( $_GET['dprcu_textures'] ) ) { $include = dprcu_get_texture_attachment_ids(); if ( !empty( $include ) ) { $query->set( 'post__in', $include ); } return; } $exclude = dprcu_get_texture_attachment_ids(); if ( empty( $exclude ) ) { return; } $existing = ( $query->get( 'post__not_in' ) ?: [] ); $query->set( 'post__not_in', [...$existing, ...$exclude] ); } add_action( 'pre_get_posts', 'dprcu_hide_from_media_library' ); // The media modal uses its own AJAX action — pre_get_posts doesn't reach it. add_filter( 'ajax_query_attachments_args', function ( $args ) { // Don't filter when the media modal is opened from a texture post itself // (featured image picker or PBR map selectors on the CPT edit screen). $post_id = ( isset( $_REQUEST['post_id'] ) ? (int) $_REQUEST['post_id'] : 0 ); if ( $post_id && 'dprcu_model_texture' === get_post_type( $post_id ) ) { return $args; } $exclude = dprcu_get_texture_attachment_ids(); if ( empty( $exclude ) ) { return $args; } $existing = $args['post__not_in'] ?? []; $args['post__not_in'] = [...$existing, ...$exclude]; return $args; } );