_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;
} );