hydrate_all(); } public function get_cached_materials(): array { return $this->hydrate_all(); } public function get_material(string $identifier): ?Material { $materials = $this->hydrate_all(); if (isset($materials[$identifier])) { return $materials[$identifier]; } $needle = strtolower(trim($identifier)); foreach ($materials as $material) { if ($needle === strtolower($material->slug) || $needle === strtolower($material->display_id())) { return $material; } } return null; } public function save(array $input): string { $items = $this->raw_items(); $originalSlug = sanitize_key((string) ($input['original_slug'] ?? '')); $slug = sanitize_key((string) ($input['slug'] ?? '')); if ($slug === '') { $slug = sanitize_key((string) ($input['name'] ?? 'material')); } if ($slug === '') { $slug = 'material-' . wp_generate_password(6, false, false); } if ($originalSlug !== '' && $originalSlug !== $slug) { unset($items[$originalSlug]); } $existing = is_array($items[$slug] ?? null) ? $items[$slug] : []; $sourceId = absint($existing['source_id'] ?? 0); if ($sourceId < 1) { $sourceId = absint(get_option('ds82_3dps_internal_material_next_id', 1)); update_option('ds82_3dps_internal_material_next_id', $sourceId + 1, false); } $colorMode = sanitize_key((string) ($input['color_mode'] ?? 'single')); $allowedModes = ['single', 'dual', 'triple', 'quad']; if (!in_array($colorMode, $allowedModes, true)) { $colorMode = 'single'; } $layoutType = sanitize_key((string) ($input['color_mode_type'] ?? 'standard')); if (!in_array($layoutType, ['standard', 'gradient', 'rainbow', 'layered'], true)) { $layoutType = 'standard'; } // Standard layout is always single-color. All effect layouts require // at least two colors so their visual meaning remains unambiguous. if ($layoutType === 'standard') { $colorMode = 'single'; } elseif ($colorMode === 'single') { $colorMode = 'dual'; } $requiredColorCount = $this->color_count($colorMode); $colors = []; foreach (['base_color', 'color_2', 'color_3', 'color_4'] as $key) { $value = sanitize_hex_color((string) ($input[$key] ?? '')); if ($value) { $colors[] = $value; } } $colors = array_slice(array_values($colors), 0, $requiredColorCount); while (count($colors) < $requiredColorCount) { $colors[] = '#b7b7b7'; } $items[$slug] = [ 'source_id' => $sourceId, 'slug' => $slug, 'name' => sanitize_text_field((string) ($input['name'] ?? '')), 'material_type' => sanitize_text_field((string) ($existing['material_type'] ?? '')), 'appearance_type' => sanitize_key((string) ($input['appearance_type'] ?? 'standard')), 'color_mode' => $colorMode, 'color_mode_type' => $layoutType, 'color_direction' => in_array(sanitize_key((string) ($input['color_direction'] ?? '')), ['left_right', 'right_left', 'front_back', 'back_front', 'bottom_top', 'top_bottom'], true) ? sanitize_key((string) $input['color_direction']) : ($layoutType === 'rainbow' ? 'bottom_top' : 'left_right'), 'color_blend_percent' => min(100, max(0, (float) ($input['color_blend_percent'] ?? 45))), 'color_shares' => array_map( static fn ($value): float => min(100, max(1, (float) $value)), [ $input['color_share_1'] ?? 25, $input['color_share_2'] ?? 25, $input['color_share_3'] ?? 25, $input['color_share_4'] ?? 25, ] ), 'layer_band_percent' => min(50, max(0.25, (float) ($input['layer_band_percent'] ?? 1))), 'layer_variation_percent' => min(25, max(0, (float) ($input['layer_variation_percent'] ?? 10))), 'layer_blend_percent' => min(20, max(0, (float) ($input['layer_blend_percent'] ?? 6))), 'manufacturer' => sanitize_text_field((string) ($existing['manufacturer'] ?? '')), 'base_color' => $colors[0] ?? '#b7b7b7', 'secondary_colors' => array_slice($colors, 1), 'background_color' => sanitize_hex_color((string) ($input['background_color'] ?? '')) ?: '', 'texture_url' => esc_url_raw((string) ($input['texture_url'] ?? '')), 'normal_map_url' => esc_url_raw((string) ($input['normal_map_url'] ?? '')), 'preview_image_url' => esc_url_raw((string) ($input['preview_image_url'] ?? '')), 'roughness' => min(1, max(0, (float) ($input['roughness'] ?? 0.55))), 'metalness' => min(1, max(0, (float) ($input['metalness'] ?? 0.05))), 'opacity' => min(1, max(0, (float) ($input['opacity'] ?? 1))), 'transparent' => isset($input['transparent']) && (string) $input['transparent'] === 'yes', 'advanced_enabled' => isset($input['advanced_enabled']) && (string) $input['advanced_enabled'] === 'yes', 'clearcoat' => min(1, max(0, (float) ($input['clearcoat'] ?? 0))), 'sheen' => min(1, max(0, (float) ($input['sheen'] ?? 0))), 'iridescence' => min(1, max(0, (float) ($input['iridescence'] ?? 0))), 'transmission' => min(1, max(0, (float) ($input['transmission'] ?? 0))), 'ior' => min(2.5, max(1, (float) ($input['ior'] ?? 1.5))), 'light_key' => min(3, max(0, (float) ($input['light_key'] ?? 1.55))), 'light_fill' => min(2, max(0, (float) ($input['light_fill'] ?? 0.55))), 'light_ambient' => min(2, max(0, (float) ($input['light_ambient'] ?? 0.85))), 'exposure' => min(2, max(0.25, (float) ($input['exposure'] ?? 1))), 'available' => isset($input['available']) && (string) $input['available'] === 'yes', 'surcharge' => (float) ($input['surcharge'] ?? 0), ]; update_option(self::OPTION, $items, false); return $slug; } public function delete(string $slug): void { $slug = sanitize_key($slug); $items = $this->raw_items(); unset($items[$slug]); update_option(self::OPTION, $items, false); } public function raw(string $slug): array { $items = $this->raw_items(); $data = is_array($items[$slug] ?? null) ? $items[$slug] : []; return $this->normalize_legacy_layout($data); } public function status(): array { return [ 'ok' => true, 'message' => sprintf( /* translators: %d: material count */ _n('%d internal material is available.', '%d internal materials are available.', count($this->raw_items()), '3d-print-showcase'), count($this->raw_items()) ), ]; } private function raw_items(): array { $items = get_option(self::OPTION, []); return is_array($items) ? $items : []; } /** @return array */ private function hydrate_all(): array { $result = []; foreach ($this->raw_items() as $slug => $data) { if (!is_array($data)) { continue; } $data = $this->normalize_legacy_layout($data); $slug = sanitize_key((string) ($data['slug'] ?? $slug)); $material = new Material( source: 'internal', sourceId: absint($data['source_id'] ?? 0), slug: $slug, name: (string) ($data['name'] ?? $slug), materialType: $this->nullable($data['material_type'] ?? null), appearanceType: $this->nullable($data['appearance_type'] ?? null), colorMode: $this->nullable($data['color_mode'] ?? null), colorModeType: $this->nullable($data['color_mode_type'] ?? null), manufacturer: $this->nullable($data['manufacturer'] ?? null), baseColor: sanitize_hex_color((string) ($data['base_color'] ?? '')) ?: '#b7b7b7', secondaryColors: array_slice(array_values(array_filter(array_map('sanitize_hex_color', (array) ($data['secondary_colors'] ?? [])))), 0, max(0, $this->color_count((string) ($data['color_mode'] ?? 'single')) - 1)), backgroundColor: sanitize_hex_color((string) ($data['background_color'] ?? '')) ?: null, textureUrl: $this->nullable($data['texture_url'] ?? null), normalMapUrl: $this->nullable($data['normal_map_url'] ?? null), roughness: (float) ($data['roughness'] ?? 0.55), metalness: (float) ($data['metalness'] ?? 0.05), opacity: (float) ($data['opacity'] ?? 1), transparent: !empty($data['transparent']), previewImageUrl: $this->nullable($data['preview_image_url'] ?? null), sourceData: $data ); $result[$material->identifier()] = $material; } return $result; } private function normalize_legacy_layout(array $data): array { $layout = sanitize_key((string) ($data['color_mode_type'] ?? 'standard')); if (!in_array($layout, ['standard', 'gradient', 'rainbow', 'layered'], true)) { $layout = 'standard'; $data['color_mode_type'] = 'standard'; } if (!in_array(sanitize_key((string) ($data['color_direction'] ?? '')), ['left_right', 'right_left', 'front_back', 'back_front', 'bottom_top', 'top_bottom'], true)) { $data['color_direction'] = $layout === 'rainbow' ? 'bottom_top' : 'left_right'; } $data['color_blend_percent'] = min(100, max(0, (float) ($data['color_blend_percent'] ?? 45))); $shares = is_array($data['color_shares'] ?? null) ? array_values($data['color_shares']) : []; $data['color_shares'] = []; for ($index = 0; $index < 4; $index++) { $data['color_shares'][] = min(100, max(1, (float) ($shares[$index] ?? 25))); } if ($layout === 'standard') { $data['color_mode'] = 'single'; $data['secondary_colors'] = []; } elseif (sanitize_key((string) ($data['color_mode'] ?? 'single')) === 'single') { $data['color_mode'] = 'dual'; $data['secondary_colors'] = ['#b7b7b7']; } return $data; } private function color_count(string $mode): int { return match (sanitize_key($mode)) { 'dual' => 2, 'triple' => 3, 'quad', 'multi' => 4, default => 1, }; } private function nullable(mixed $value): ?string { $value = trim((string) $value); return $value === '' ? null : $value; } }