*/ public function get_categories(): array { return ['general']; } /** * Get widget keywords. * * @return array */ public function get_keywords(): array { return ['3d', '3d embed', '3d viewer', '3d model', 'model viewer', 'glb', 'gltf', 'obj', 'stl', 'fbx', 'ar', 'augmented reality', '360', 'embed']; } /** * Get widget script dependencies. * * @return array */ public function get_script_depends(): array { return ['bp3d-public']; } /** * Get widget style dependencies. * * @return array */ public function get_style_depends(): array { return ['bp3d-frontend']; } /** * Register widget controls. */ protected function register_controls(): void { $this->registerContentControls(); $this->registerStyleControls(); } /** * Register Content tab controls. */ private function registerContentControls(): void { $this->start_controls_section('embedder', [ 'label' => esc_html__('Model Viewer', '3d-viewer'), 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, ]); // Allowed upload formats notice $allowed_mimes = \BP3D\Helper\Utils::getAllowedMimeTypes(); $settings_url = admin_url('edit.php?post_type=bp3d-model-viewer&page=3dviewer-settings'); if (empty($allowed_mimes)) { $format_notice = sprintf( /* translators: %s: URL to the settings page. */ __('Notice: All 3D file formats are currently disabled for upload. Please enable the formats you need in the 3D Viewer Settings.', '3d-viewer'), esc_url($settings_url) ); $notice_type = 'danger'; } else { $format_notice = \BP3D\Helper\Utils::enabledFormatsNote() ?: sprintf( /* translators: %s: URL to the settings page. */ __('All supported 3D formats (GLB, GLTF, OBJ, STL, FBX, HDR, etc.) are enabled for upload by default. You can turn any of them off in the 3D Viewer Settings.', '3d-viewer'), esc_url($settings_url) ); $notice_type = 'info'; } $this->add_control('format_notice', [ 'type' => \Elementor\Controls_Manager::RAW_HTML, 'raw' => $format_notice, 'content_classes' => 'elementor-panel-alert elementor-panel-alert-' . $notice_type, ]); // Viewer type $this->add_control('currentViewer', [ 'label' => esc_html__('Viewer', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SELECT, 'default' => 'modelViewer', 'options' => [ 'modelViewer' => __('Lite', '3d-viewer'), 'O3DViewer' => __('Advanced', '3d-viewer'), ], ]); // Single model controls $this->add_control('modelUrl', [ 'label' => esc_html__('Select Model', '3d-viewer'), 'type' => 'bp3d-select-file', 'library' => \BP3D\Helper\Utils::modelLibraryTypes(), 'separator' => 'before', 'placeholder' => esc_html__('Paste Model URL', '3d-viewer'), ]); $this->add_control('useDecoder', [ 'label' => esc_html__('Use Decoder', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SELECT, 'default' => 'none', 'options' => [ 'none' => esc_html__('None', '3d-viewer'), 'draco' => esc_html__('Draco', '3d-viewer'), ], 'condition' => ['currentViewer' => 'modelViewer'], ]); $this->add_control('bin_file', [ 'label' => esc_html__('Upload bin file', '3d-viewer'), 'type' => 'bp3d-select-file', 'separator' => 'before', 'placeholder' => esc_html__('Paste bin file URL', '3d-viewer'), 'condition' => ['useDecoder' => 'draco', 'currentViewer' => 'modelViewer'], ]); $this->add_control('poster', [ 'label' => esc_html__('Select Poster', '3d-viewer'), 'type' => 'bp3d-select-file', 'placeholder' => esc_html__('Paste Poster URL', '3d-viewer'), 'condition' => ['currentViewer' => 'modelViewer'], ]); $this->add_control('environmentImagePreset', [ 'label' => esc_html__('Environment Image', '3d-viewer'), 'description' => esc_html__('Improves lighting and reflections on the model.', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SELECT, 'default' => 'neutral', 'options' => [ 'neutral' => esc_html__('Neutral', '3d-viewer'), 'legacy' => esc_html__('Legacy', '3d-viewer'), 'custom' => esc_html__('Custom', '3d-viewer'), ], 'condition' => ['currentViewer' => 'modelViewer'], ]); $this->add_control('environmentImage', [ 'label' => esc_html__('Custom Environment Image', '3d-viewer'), 'type' => 'bp3d-select-file', 'placeholder' => esc_html__('Paste Image URL', '3d-viewer'), 'condition' => ['currentViewer' => 'modelViewer', 'environmentImagePreset' => 'custom'], ]); $this->add_control('skyboxImage', [ 'label' => esc_html__('HDR Skybox Image', '3d-viewer'), 'description' => esc_html__('Shown as the background and used for environmental lighting. Accepts .hdr as well as JPG and PNG.', '3d-viewer'), 'type' => 'bp3d-select-file', 'placeholder' => esc_html__('Paste Skybox Image URL', '3d-viewer'), 'condition' => ['currentViewer' => 'modelViewer'], ]); // Divider $this->add_control('hr', [ 'type' => \Elementor\Controls_Manager::DIVIDER, ]); // Feature toggles $this->add_control('fullscreen', [ 'label' => esc_html__('Fullscreen Button', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'return_value' => 'yes', 'default' => 'yes', ]); $this->add_control('mouseControls', [ 'label' => esc_html__('Mouse Control', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => esc_html__('Enable', '3d-viewer'), 'label_off' => esc_html__('Disable', '3d-viewer'), 'return_value' => 'yes', 'default' => 'yes', ]); $this->add_control('lazy_load', [ 'label' => esc_html__('Lazy Load', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => esc_html__('Enable', '3d-viewer'), 'label_off' => esc_html__('Disable', '3d-viewer'), 'return_value' => 'yes', 'default' => false, 'condition' => ['currentViewer' => 'modelViewer'], ]); $this->add_control('progressBar', [ 'label' => esc_html__('Show Progress Bar', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'return_value' => 'yes', 'default' => 'yes', 'condition' => ['currentViewer' => 'modelViewer'], ]); $this->add_control('zoom', [ 'label' => esc_html__('Enable Zoom', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => esc_html__('Enable', '3d-viewer'), 'label_off' => esc_html__('Disable', '3d-viewer'), 'return_value' => 'yes', 'default' => 'yes', 'condition' => ['currentViewer' => 'modelViewer'], ]); $this->add_control('zoomInOutBtn', [ 'label' => esc_html__('Zoom In/Out Button', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => esc_html__('Show', '3d-viewer'), 'label_off' => esc_html__('Hide', '3d-viewer'), 'return_value' => 'yes', 'default' => 'no', ]); $this->add_control('cameraBtn', [ 'label' => esc_html__('Camera/Capture Button', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => esc_html__('Show', '3d-viewer'), 'label_off' => esc_html__('Hide', '3d-viewer'), 'return_value' => 'yes', 'default' => 'no', 'condition' => ['currentViewer' => 'modelViewer'], ]); $this->add_control('downloadBtn', [ 'label' => esc_html__('3D File Download Button', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => esc_html__('Show', '3d-viewer'), 'label_off' => esc_html__('Hide', '3d-viewer'), 'return_value' => 'yes', 'default' => 'no', ]); $this->add_control('loadingPercentage', [ 'label' => esc_html__('Show Loading Percentage', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => esc_html__('Show', '3d-viewer'), 'label_off' => esc_html__('Hide', '3d-viewer'), 'return_value' => 'yes', 'default' => 'no', 'condition' => ['currentViewer' => 'modelViewer'], ]); $this->add_control('shadow', [ 'label' => esc_html__('Enable Shadow', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => esc_html__('Enable', '3d-viewer'), 'label_off' => esc_html__('Disable', '3d-viewer'), 'return_value' => 'yes', 'default' => 'yes', 'condition' => ['currentViewer' => 'modelViewer'], ]); $this->add_control('exposure', [ 'label' => esc_html__('Exposure', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SLIDER, 'range' => [ 'px' => [ 'min' => 0.1, 'max' => 5, 'step' => 0.1, ], ], 'default' => [ 'size' => 1, ], 'condition' => ['currentViewer' => 'modelViewer'], ]); // Divider $this->add_control('hr_ar', [ 'type' => \Elementor\Controls_Manager::DIVIDER, 'condition' => ['currentViewer' => 'modelViewer'], ]); $this->add_control('enableAr', [ 'label' => esc_html__('Enable AR', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => esc_html__('Enable', '3d-viewer'), 'label_off' => esc_html__('Disable', '3d-viewer'), 'return_value' => 'yes', 'default' => 'no', 'condition' => ['currentViewer' => 'modelViewer'], ]); $this->add_control('modelIsoSrc', [ 'label' => esc_html__('3D Source for iOS (Optional)', '3d-viewer'), 'type' => 'bp3d-select-file', 'placeholder' => esc_html__('Paste USDZ Model URL', '3d-viewer'), 'condition' => [ 'currentViewer' => 'modelViewer', 'enableAr' => 'yes', ], ]); $this->add_control('arPlacement', [ 'label' => esc_html__('AR Placement', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SELECT, 'default' => 'floor', 'options' => [ 'floor' => esc_html__('Floor', '3d-viewer'), 'wall' => esc_html__('Wall', '3d-viewer'), ], 'condition' => [ 'currentViewer' => 'modelViewer', 'enableAr' => 'yes', ], ]); $this->add_control('arMode', [ 'label' => esc_html__('AR Mode', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SELECT, 'default' => 'webxr', 'options' => [ 'webxr' => esc_html__('WebXR', '3d-viewer'), 'scene-viewer' => esc_html__('Scene Viewer', '3d-viewer'), 'quick-look' => esc_html__('Quick Look', '3d-viewer'), ], 'condition' => [ 'currentViewer' => 'modelViewer', 'enableAr' => 'yes', ], ]); $this->end_controls_section(); } /** * Register Style tab controls. */ private function registerStyleControls(): void { $this->start_controls_section('model', [ 'label' => esc_html__('Model', '3d-viewer'), 'tab' => \Elementor\Controls_Manager::TAB_STYLE, ]); // Width $this->add_control('width', [ 'label' => esc_html__('Width', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SLIDER, 'size_units' => ['px', '%', 'vw'], 'range' => [ 'px' => ['min' => 0, 'max' => 1000, 'step' => 5], '%' => ['min' => 20, 'max' => 100], 'vw' => ['min' => 5, 'max' => 100], ], 'default' => ['unit' => '%', 'size' => 100], 'selectors' => [ '{{WRAPPER}} .b3dviewer model-viewer' => 'width:100%;max-width:100%;', '{{WRAPPER}} .b3dviewer .bp_model_parent' => 'width: {{SIZE}}{{UNIT}} !important;margin:0 auto;max-width:100%;', ], ]); // Height $this->add_control('height', [ 'label' => esc_html__('Height', '3d-viewer'), 'type' => \Elementor\Controls_Manager::SLIDER, 'size_units' => ['px', 'vh'], 'range' => [ 'px' => ['min' => 200, 'max' => 1000, 'step' => 5], 'vh' => ['min' => 5, 'max' => 100], ], 'default' => ['unit' => 'px', 'size' => 500], 'selectors' => [ '{{WRAPPER}} .b3dviewer model-viewer' => 'height: {{SIZE}}{{UNIT}};', '{{WRAPPER}} .b3dviewer model-viewer #lazy-load-poster img' => 'height: {{SIZE}}{{UNIT}};', ], ]); // Background color $this->add_control('backgroundColor', [ 'label' => esc_html__('Background Color', '3d-viewer'), 'type' => \Elementor\Controls_Manager::COLOR, 'selectors' => [ '{{WRAPPER}} .b3dviewer model-viewer' => 'background: {{VALUE}}', '{{WRAPPER}} .b3dviewer .bp_model_parent' => 'background: {{VALUE}}', ], ]); $this->end_controls_section(); } /** * Create a settings accessor closure. */ public function bp3d_get_settings(): \Closure { $settings = $this->get_settings_for_display(); return function ($key, $default = false, $is_boolean = false, $key2 = null) use ($settings) { if (isset($settings[$key], $settings[$key][$key2])) { return $is_boolean ? ($settings[$key][$key2] === 'yes') : $settings[$key][$key2]; } if (isset($settings[$key])) { return $is_boolean ? ($settings[$key] === 'yes') : $settings[$key]; } return $default; }; } /** * Render the widget output. */ protected function render(): void { $settings = $this->get_settings_for_display(); $get_settings = $this->bp3d_get_settings(); $environment_image = \BP3D\Helper\Utils::resolveEnvironmentImage( $settings['environmentImagePreset'] ?? null, $settings['environmentImage'] ?? '' ); $finalData = [ 'align' => 'center', 'uniqueId' => 'b3dviewer' . uniqid(), 'O3DVSettings' => [ 'isFullscreen' => ($settings['fullscreen'] ?? '') === 'yes', 'camera' => null, 'mouseControl' => ($settings['mouseControls'] ?? '') === 'yes', 'zoom' => ($settings['zoom'] ?? '') === 'yes', ], 'model' => [ 'modelUrl' => $settings['modelUrl'] ?? '', 'poster' => $settings['poster'] ?? '', 'environmentImage' => $environment_image, 'skyboxImage' => $settings['skyboxImage'] ?? '', // The frontend renderer reads `model.decoder` and the model-viewer // build expects the "Draco"/"none" spelling used by the block editor, // so map the widget's lowercase `useDecoder` value onto that contract. 'decoder' => (($settings['useDecoder'] ?? 'none') === 'draco') ? 'Draco' : 'none', 'arEnabled' => ($settings['enableAr'] ?? '') === 'yes', 'arPlacement' => $settings['arPlacement'] ?? 'floor', 'arMode' => $settings['arMode'] ?? 'webxr', 'modelISOSrc' => \BP3D\Helper\Utils::safeMediaUrl($settings['modelIsoSrc'] ?? ''), ], 'currentViewer' => $settings['currentViewer'] ?? 'modelViewer', 'loading' => ($settings['lazy_load'] ?? '') === 'yes' ? 'lazy' : 'eager', 'zoom' => ($settings['zoom'] ?? '') === 'yes', 'zoomInOutBtn' => ($settings['zoomInOutBtn'] ?? '') === 'yes', 'cameraBtn' => ($settings['cameraBtn'] ?? '') === 'yes', 'downloadBtn' => ($settings['downloadBtn'] ?? '') === 'yes', 'preload' => 'auto', 'mouseControl' => ($settings['mouseControls'] ?? '') === 'yes', 'fullscreen' => ($settings['fullscreen'] ?? '') === 'yes', 'loadingPercentage' => ($settings['loadingPercentage'] ?? '') === 'yes', 'progressBar' => ($settings['progressBar'] ?? '') === 'yes', 'environmentImage' => $environment_image, 'exposure' => isset($settings['exposure']['size']) ? (float) $settings['exposure']['size'] : 1.0, 'shadow' => ($settings['shadow'] ?? '') === 'yes', 'styles' => [ 'width' => '100%', 'height' => $get_settings('height', '500', false, 'size') . $get_settings('height', 'px', false, 'unit'), 'bgColor' => $settings['backgroundColor'] ?? 'transparent', 'bgImage' => $settings['backgroundImage']['url'] ?? '', 'progressBarColor' => '#666', ], ]; // Without a model the renderer outputs nothing, which collapses the // widget to Elementor's icon placeholder. Hold the configured height // in the editor instead; visitors still get no empty box. if (empty($finalData['model']['modelUrl'])) { if ($this->isElementorEditor()) { $this->renderEmptyPlaceholder($finalData['styles']['height']); } return; } Addons::ensureElementorDependencies(); Addons::enqueueViewerLibrary($finalData['currentViewer']); ?>
editor) && $plugin->editor->is_edit_mode()) { return true; } return isset($plugin->preview) && $plugin->preview->is_preview_mode(); } /** * Render the editor-only placeholder shown while no model is selected. * * Styles are inline: the placeholder never reaches the frontend, so it is * not worth a stylesheet the preview would have to load. */ private function renderEmptyPlaceholder(string $height): void { if (!preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $height)) { $height = '500px'; } $box = 'display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;' . 'box-sizing:border-box;padding:20px;text-align:center;' . 'border:1px dashed #c5cad1;border-radius:4px;background:#f7f8f9;' . 'min-height:' . $height . ';'; ?>