From 9d0c891d2c7b66ceacfaefc9b8c6b8112e28203c Mon Sep 17 00:00:00 2001 From: Jonas Pfalzgraf Date: Wed, 29 Oct 2025 16:57:25 +0100 Subject: [PATCH 01/19] =?UTF-8?q?F=C3=BCge=20Diagnosen=20und=20Toleranzen?= =?UTF-8?q?=20zur=20Verpackungsoptimierung=20hinzu=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Füge Diagnosen und Toleranzen zur Verpackungsoptimierung hinzu * 📈 Füge Diagnosen und Zusammenfassungen zur Verpackungsoptimierung hinzu * 🩹 Verbessere die Berechnung der Unterstützungsprozentwerte in den Diagnosen und optimiere die Handhabung von Unterstützungsproben --- src/api.rs | 45 +++-- src/config.rs | 9 + src/optimizer.rs | 516 ++++++++++++++++++++++++++++++++++++++++++++--- web/script.js | 124 ++++++++++++ 4 files changed, 655 insertions(+), 39 deletions(-) diff --git a/src/api.rs b/src/api.rs index e1f2fa7..6e7ed2d 100644 --- a/src/api.rs +++ b/src/api.rs @@ -20,7 +20,10 @@ use tower_http::cors::{Any, CorsLayer}; use crate::config::{ApiConfig, OptimizerConfig}; use crate::model::{Box3D, ContainerBlueprint, ValidationError}; -use crate::optimizer::{pack_objects_with_config, pack_objects_with_progress, PackingResult}; +use crate::optimizer::{ + pack_objects_with_config, pack_objects_with_progress, ContainerDiagnostics, + PackingDiagnosticsSummary, PackingResult, +}; #[derive(Clone)] struct ApiState { @@ -86,6 +89,7 @@ pub struct PackResponse { pub results: Vec, pub unplaced: Vec, pub is_complete: bool, + pub diagnostics_summary: PackingDiagnosticsSummary, } /// Einzelner Container mit Metadaten und platzierten Objekten. @@ -103,6 +107,7 @@ pub struct PackedContainer { pub max_weight: f64, pub total_weight: f64, pub placed: Vec, + pub diagnostics: ContainerDiagnostics, } /// Einzelnes platziertes Objekt in der Response. @@ -132,22 +137,24 @@ pub struct PackedUnplacedObject { impl PackResponse { /// Erstellt eine PackResponse aus einem PackingResult (DRY-Prinzip). pub fn from_packing_result(result: PackingResult) -> Self { - let is_complete = result.is_complete(); - let containers = result.containers; - let unplaced_entries = result.unplaced; + let PackingResult { + containers, + unplaced, + container_diagnostics, + diagnostics_summary, + } = result; + + let is_complete = unplaced.is_empty(); + let unplaced_entries = unplaced; Self { results: containers .into_iter() + .zip(container_diagnostics.into_iter()) .enumerate() - .map(|(i, cont)| PackedContainer { - id: i + 1, - template_id: cont.template_id, - label: cont.label.clone(), - dims: cont.dims, - max_weight: cont.max_weight, - total_weight: cont.total_weight(), - placed: cont + .map(|(i, (cont, diagnostics))| { + let total_weight = cont.total_weight(); + let placed_objects = cont .placed .into_iter() .map(|p| PackedObject { @@ -156,7 +163,18 @@ impl PackResponse { weight: p.object.weight, dims: p.object.dims, }) - .collect(), + .collect(); + + PackedContainer { + id: i + 1, + template_id: cont.template_id, + label: cont.label.clone(), + dims: cont.dims, + max_weight: cont.max_weight, + total_weight, + placed: placed_objects, + diagnostics, + } }) .collect(), unplaced: unplaced_entries @@ -170,6 +188,7 @@ impl PackResponse { }) .collect(), is_complete, + diagnostics_summary, } } } diff --git a/src/config.rs b/src/config.rs index 86c74c5..329b580 100644 --- a/src/config.rs +++ b/src/config.rs @@ -166,6 +166,7 @@ impl OptimizerConfig { const HEIGHT_EPSILON_VAR: &'static str = "SORT_IT_NOW_PACKING_HEIGHT_EPSILON"; const GENERAL_EPSILON_VAR: &'static str = "SORT_IT_NOW_PACKING_GENERAL_EPSILON"; const BALANCE_RATIO_VAR: &'static str = "SORT_IT_NOW_PACKING_BALANCE_LIMIT_RATIO"; + const FOOTPRINT_TOLERANCE_VAR: &'static str = "SORT_IT_NOW_PACKING_FOOTPRINT_TOLERANCE"; fn from_env() -> Self { let mut packing = PackingConfig::default(); @@ -210,6 +211,14 @@ impl OptimizerConfig { "Warnung: Angepasste Balance-Grenzen können zum Umkippen von Stapeln führen", ); + packing.footprint_cluster_tolerance = load_f64_with_warning( + Self::FOOTPRINT_TOLERANCE_VAR, + PackingConfig::DEFAULT_FOOTPRINT_CLUSTER_TOLERANCE, + |value| (0.0..=0.5).contains(&value), + "muss zwischen 0 und 0.5 liegen", + "Warnung: Angepasste Footprint-Gruppierung kann zu unerwarteten Platzierungen führen", + ); + Self { packing } } diff --git a/src/optimizer.rs b/src/optimizer.rs index 8d2b724..572de31 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -27,6 +27,8 @@ pub struct PackingConfig { pub general_epsilon: f64, /// Maximale erlaubte Abweichung des Schwerpunkts vom Mittelpunkt (als Ratio der Diagonale) pub balance_limit_ratio: f64, + /// Relative Toleranz bei der Vorgruppierung nach Grundfläche zur Reduktion von Backtracking + pub footprint_cluster_tolerance: f64, } impl PackingConfig { @@ -35,6 +37,7 @@ impl PackingConfig { pub const DEFAULT_HEIGHT_EPSILON: f64 = 1e-3; pub const DEFAULT_GENERAL_EPSILON: f64 = 1e-6; pub const DEFAULT_BALANCE_LIMIT_RATIO: f64 = 0.45; + pub const DEFAULT_FOOTPRINT_CLUSTER_TOLERANCE: f64 = 0.15; /// Erstellt einen Builder für benutzerdefinierte Konfiguration. pub fn builder() -> PackingConfigBuilder { @@ -50,6 +53,7 @@ impl Default for PackingConfig { height_epsilon: Self::DEFAULT_HEIGHT_EPSILON, general_epsilon: Self::DEFAULT_GENERAL_EPSILON, balance_limit_ratio: Self::DEFAULT_BALANCE_LIMIT_RATIO, + footprint_cluster_tolerance: Self::DEFAULT_FOOTPRINT_CLUSTER_TOLERANCE, } } } @@ -99,17 +103,151 @@ impl PackingConfigBuilder { self } + /// Setzt die Toleranz für die Vorgruppierung basierend auf der Grundfläche. + pub fn footprint_cluster_tolerance(mut self, tolerance: f64) -> Self { + self.config.footprint_cluster_tolerance = tolerance; + self + } + /// Erstellt die finale Konfiguration. pub fn build(self) -> PackingConfig { self.config } } +/// Abstrakte Strategien zur Gruppierung/Neuordnung von Objekten vor dem Packen. +trait ObjectClusterStrategy { + fn reorder(&self, objects: Vec) -> Vec; +} + +/// Gruppiert Objekte mit kompatibler Grundfläche, um Backtracking zu reduzieren. +#[derive(Clone, Debug)] +struct FootprintClusterStrategy { + tolerance: f64, +} + +impl FootprintClusterStrategy { + fn new(tolerance: f64) -> Self { + Self { tolerance } + } + + fn compatible(&self, a: (f64, f64), b: (f64, f64)) -> bool { + if self.tolerance <= 0.0 { + return false; + } + + let width_close = self.relative_diff(a.0, b.0) <= self.tolerance; + let depth_close = self.relative_diff(a.1, b.1) <= self.tolerance; + width_close && depth_close + } + + fn relative_diff(&self, a: f64, b: f64) -> f64 { + let denom = a.abs().max(b.abs()).max(1.0); + (a - b).abs() / denom + } +} + +impl ObjectClusterStrategy for FootprintClusterStrategy { + fn reorder(&self, objects: Vec) -> Vec { + if self.tolerance <= 0.0 { + return objects; + } + + let mut clusters: Vec = Vec::new(); + for object in objects.into_iter() { + let dims = (object.dims.0, object.dims.1); + if let Some(cluster) = clusters + .iter_mut() + .find(|cluster| self.compatible(cluster.representative, dims)) + { + cluster.add(object); + } else { + clusters.push(ObjectCluster::new(object)); + } + } + + clusters + .into_iter() + .flat_map(ObjectCluster::into_members) + .collect() + } +} + +#[derive(Clone, Debug)] +struct ObjectCluster { + representative: (f64, f64), + members: Vec, +} + +impl ObjectCluster { + fn new(object: Box3D) -> Self { + let dims = (object.dims.0, object.dims.1); + Self { + representative: dims, + members: vec![object], + } + } + + fn add(&mut self, object: Box3D) { + let dims = (object.dims.0, object.dims.1); + let count = self.members.len() as f64; + let (rw, rd) = self.representative; + self.representative = ( + (rw * count + dims.0) / (count + 1.0), + (rd * count + dims.1) / (count + 1.0), + ); + self.members.push(object); + } + + fn into_members(self) -> Vec { + self.members + } +} + +/// Support-Kennzahlen pro Objekt. +#[derive(Clone, Debug, serde::Serialize)] +pub struct SupportDiagnostics { + pub object_id: usize, + pub support_percent: f64, + pub rests_on_floor: bool, +} + +/// Diagnostische Kennzahlen pro Container für Monitoring. +#[derive(Clone, Debug, serde::Serialize)] +pub struct ContainerDiagnostics { + pub center_of_mass_offset: f64, + pub balance_limit: f64, + pub imbalance_ratio: f64, + pub average_support_percent: f64, + pub minimum_support_percent: f64, + pub support_samples: Vec, +} + +/// Zusammenfassung wichtiger Kennzahlen über alle Container hinweg. +#[derive(Clone, Debug, serde::Serialize)] +pub struct PackingDiagnosticsSummary { + pub max_imbalance_ratio: f64, + pub worst_support_percent: f64, + pub average_support_percent: f64, +} + +impl Default for PackingDiagnosticsSummary { + fn default() -> Self { + Self { + max_imbalance_ratio: 0.0, + worst_support_percent: 100.0, + average_support_percent: 100.0, + } + } +} + /// Ergebnis der Verpackungsberechnung. #[derive(Clone, Debug)] pub struct PackingResult { pub containers: Vec, pub unplaced: Vec, + pub container_diagnostics: Vec, + pub diagnostics_summary: PackingDiagnosticsSummary, } impl PackingResult { @@ -145,6 +283,11 @@ impl PackingResult { pub fn total_packed_weight(&self) -> f64 { self.containers.iter().map(|c| c.total_weight()).sum() } + + /// Liefert die aggregierten Diagnosewerte. + pub fn diagnostics_summary(&self) -> &PackingDiagnosticsSummary { + &self.diagnostics_summary + } } /// Objekt, das nicht platziert werden konnte. @@ -277,6 +420,11 @@ pub enum PackEvent { dims: (f64, f64, f64), total_weight: f64, }, + /// Aktualisierte Diagnostik eines Containers. + ContainerDiagnostics { + container_id: usize, + diagnostics: ContainerDiagnostics, + }, /// Ein Objekt konnte nicht platziert werden. ObjectRejected { id: usize, @@ -286,7 +434,11 @@ pub enum PackEvent { reason_text: String, }, /// Packen abgeschlossen. - Finished { containers: usize, unplaced: usize }, + Finished { + containers: usize, + unplaced: usize, + diagnostics_summary: PackingDiagnosticsSummary, + }, } /// Verpackung mit benutzerdefinierter Konfiguration und Live-Progress Callback. @@ -302,10 +454,13 @@ pub fn pack_objects_with_progress( on_event(&PackEvent::Finished { containers: 0, unplaced: 0, + diagnostics_summary: PackingDiagnosticsSummary::default(), }); return PackingResult { containers: Vec::new(), unplaced: Vec::new(), + container_diagnostics: Vec::new(), + diagnostics_summary: PackingDiagnosticsSummary::default(), }; } @@ -327,10 +482,13 @@ pub fn pack_objects_with_progress( on_event(&PackEvent::Finished { containers: 0, unplaced: unplaced.len(), + diagnostics_summary: PackingDiagnosticsSummary::default(), }); return PackingResult { containers: Vec::new(), unplaced, + container_diagnostics: Vec::new(), + diagnostics_summary: PackingDiagnosticsSummary::default(), }; } @@ -360,8 +518,12 @@ pub fn pack_objects_with_progress( .then_with(|| a.id.cmp(&b.id)) }); + let cluster_strategy = FootprintClusterStrategy::new(config.footprint_cluster_tolerance); + objects = cluster_strategy.reorder(objects); + let mut containers: Vec = Vec::new(); let mut unplaced: Vec = Vec::new(); + let mut container_diagnostics: Vec = Vec::new(); // Platziere jedes Objekt for obj in objects { @@ -393,6 +555,23 @@ pub fn pack_objects_with_progress( dims: containers[idx].placed.last().unwrap().object.dims, total_weight: total_w, }); + let diagnostics = compute_container_diagnostics(&containers[idx], &config); + if let Some(slot) = container_diagnostics.get_mut(idx) { + *slot = diagnostics.clone(); + } else if idx == container_diagnostics.len() { + container_diagnostics.push(diagnostics.clone()); + } else { + debug_assert!( + false, + "diagnostics vector out of sync with containers (idx = {}, len = {})", + idx, + container_diagnostics.len() + ); + } + on_event(&PackEvent::ContainerDiagnostics { + container_id: idx + 1, + diagnostics, + }); } else { match templates.iter().position(|tpl| tpl.can_fit(&obj)) { Some(template_index) => { @@ -430,6 +609,15 @@ pub fn pack_objects_with_progress( dims: placed.object.dims, total_weight: total_w, }); + let diagnostics = containers + .last() + .map(|c| compute_container_diagnostics(c, &config)) + .expect("missing container for diagnostics"); + container_diagnostics.push(diagnostics.clone()); + on_event(&PackEvent::ContainerDiagnostics { + container_id: new_id, + diagnostics, + }); } None => { let reason = UnplacedReason::NoStablePosition; @@ -465,13 +653,17 @@ pub fn pack_objects_with_progress( } } + let diagnostics_summary = summarize_diagnostics(container_diagnostics.iter()); on_event(&PackEvent::Finished { containers: containers.len(), unplaced: unplaced.len(), + diagnostics_summary: diagnostics_summary.clone(), }); PackingResult { containers, unplaced, + container_diagnostics, + diagnostics_summary, } } @@ -664,31 +856,43 @@ fn supports_weight_correctly(b: &PlacedBox, cont: &Container, config: &PackingCo /// * `b` - Das zu prüfende platzierte Objekt /// * `cont` - Der Container /// * `config` - Konfigurationsparameter -fn has_sufficient_support(b: &PlacedBox, cont: &Container, config: &PackingConfig) -> bool { +fn support_ratio_of(b: &PlacedBox, cont: &Container, config: &PackingConfig) -> f64 { if b.position.2 <= config.height_epsilon { - return true; + return 1.0; } - let mut support_area = 0.0; let (bx, by, bz) = b.position; let (bw, bd, _) = b.object.dims; + let base_area = bw * bd; + if base_area <= config.general_epsilon { + return 0.0; + } + + let mut support_area = 0.0; for p in &cont.placed { + let support_surface_z = p.position.2 + p.object.dims.2; + if (bz - support_surface_z).abs() > config.height_epsilon { + continue; + } + let over_x = overlap_1d(bx, bx + bw, p.position.0, p.position.0 + p.object.dims.0); let over_y = overlap_1d(by, by + bd, p.position.1, p.position.1 + p.object.dims.1); - let diff_z = bz - (p.position.2 + p.object.dims.2); - if diff_z.abs() < config.height_epsilon && over_x > 0.0 && over_y > 0.0 { + if over_x > 0.0 && over_y > 0.0 { support_area += over_x * over_y; } } - let base_area = bw * bd; - if base_area <= config.general_epsilon { - return false; + (support_area / base_area).clamp(0.0, 1.0) +} + +fn has_sufficient_support(b: &PlacedBox, cont: &Container, config: &PackingConfig) -> bool { + if b.position.2 <= config.height_epsilon { + return true; } - (support_area / base_area) >= config.support_ratio + support_ratio_of(b, cont, config) + config.general_epsilon >= config.support_ratio } /// Prüft, ob der Schwerpunkt des Objekts (Projektion auf XY) von der Auflagefläche getragen wird. @@ -728,24 +932,27 @@ fn is_center_supported(b: &PlacedBox, cont: &Container, config: &PackingConfig) /// * `cont` - Der Container /// * `new_box` - Das hinzuzufügende Objekt fn calculate_balance_after(cont: &Container, new_box: &PlacedBox) -> f64 { - let mut total_w = new_box.object.weight; - let mut x_c = (new_box.position.0 + new_box.object.dims.0 / 2.0) * new_box.object.weight; - let mut y_c = (new_box.position.1 + new_box.object.dims.1 / 2.0) * new_box.object.weight; + let new_point = ( + new_box.position.0 + new_box.object.dims.0 / 2.0, + new_box.position.1 + new_box.object.dims.1 / 2.0, + new_box.object.weight, + ); - for p in &cont.placed { - let w = p.object.weight; - total_w += w; - x_c += (p.position.0 + p.object.dims.0 / 2.0) * w; - y_c += (p.position.1 + p.object.dims.1 / 2.0) * w; + match compute_center_of_mass_xy( + cont.placed + .iter() + .map(|p| { + ( + p.position.0 + p.object.dims.0 / 2.0, + p.position.1 + p.object.dims.1 / 2.0, + p.object.weight, + ) + }) + .chain(std::iter::once(new_point)), + ) { + Some(cm) => distance_2d(cm, container_center_xy(cont)), + None => 0.0, } - - let cm_x = x_c / total_w; - let cm_y = y_c / total_w; - - let center_x = cont.dims.0 / 2.0; - let center_y = cont.dims.1 / 2.0; - - ((cm_x - center_x).powi(2) + (cm_y - center_y).powi(2)).sqrt() } /// Bewertung einer Platzierungsposition. @@ -841,6 +1048,160 @@ fn calculate_balance_limit(cont: &Container, config: &PackingConfig) -> f64 { (half_x.powi(2) + half_y.powi(2)).sqrt() * config.balance_limit_ratio } +fn calculate_current_balance_offset(cont: &Container) -> f64 { + if cont.placed.is_empty() { + return 0.0; + } + + match compute_center_of_mass_xy(cont.placed.iter().map(|p| { + ( + p.position.0 + p.object.dims.0 / 2.0, + p.position.1 + p.object.dims.1 / 2.0, + p.object.weight, + ) + })) { + Some(cm) => distance_2d(cm, container_center_xy(cont)), + None => 0.0, + } +} + +fn container_center_xy(cont: &Container) -> (f64, f64) { + (cont.dims.0 / 2.0, cont.dims.1 / 2.0) +} + +fn distance_2d(a: (f64, f64), b: (f64, f64)) -> f64 { + ((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt() +} + +fn compute_center_of_mass_xy(points: I) -> Option<(f64, f64)> +where + I: Iterator, +{ + let mut total_w = 0.0; + let mut x_c = 0.0; + let mut y_c = 0.0; + + for (x, y, w) in points { + total_w += w; + x_c += x * w; + y_c += y * w; + } + + if total_w <= 0.0 { + None + } else { + Some((x_c / total_w, y_c / total_w)) + } +} + +/// Berechnet diagnostische Kennzahlen für einen Container. +pub fn compute_container_diagnostics( + cont: &Container, + config: &PackingConfig, +) -> ContainerDiagnostics { + let balance_limit = calculate_balance_limit(cont, config); + let center_offset = calculate_current_balance_offset(cont); + + let imbalance_ratio = if balance_limit > config.general_epsilon { + center_offset / balance_limit + } else { + 0.0 + }; + + let mut support_samples = Vec::with_capacity(cont.placed.len()); + let mut total_support = 0.0; + let mut min_support: f64 = 1.0; + + for placed in &cont.placed { + let ratio = support_ratio_of(placed, cont, config); + total_support += ratio; + min_support = min_support.min(ratio); + support_samples.push(SupportDiagnostics { + object_id: placed.object.id, + support_percent: ratio * 100.0, + rests_on_floor: placed.position.2 <= config.height_epsilon, + }); + } + + let count = cont.placed.len() as f64; + let average_support_percent = if count > 0.0 { + (total_support / count) * 100.0 + } else { + 100.0 + }; + let minimum_support_percent = if cont.placed.is_empty() { + 100.0 + } else { + min_support * 100.0 + }; + + ContainerDiagnostics { + center_of_mass_offset: center_offset, + balance_limit, + imbalance_ratio, + average_support_percent, + minimum_support_percent, + support_samples, + } +} + +struct SummaryAccumulator { + max_imbalance_ratio: f64, + worst_support_percent: f64, + support_percent_sum: f64, + support_sample_count: usize, +} + +impl SummaryAccumulator { + fn new() -> Self { + Self { + max_imbalance_ratio: 0.0, + worst_support_percent: 100.0, + support_percent_sum: 0.0, + support_sample_count: 0, + } + } + + fn record(&mut self, diagnostics: &ContainerDiagnostics) { + self.max_imbalance_ratio = self.max_imbalance_ratio.max(diagnostics.imbalance_ratio); + self.worst_support_percent = self + .worst_support_percent + .min(diagnostics.minimum_support_percent); + + let sample_count = diagnostics.support_samples.len(); + if sample_count > 0 { + self.support_percent_sum += diagnostics.average_support_percent * sample_count as f64; + self.support_sample_count += sample_count; + } + } + + fn finish(self) -> PackingDiagnosticsSummary { + let average_support_percent = if self.support_sample_count > 0 { + self.support_percent_sum / self.support_sample_count as f64 + } else { + 100.0 + }; + + PackingDiagnosticsSummary { + max_imbalance_ratio: self.max_imbalance_ratio, + worst_support_percent: self.worst_support_percent, + average_support_percent, + } + } +} + +/// Aggregiert Diagnosen über mehrere Container. +pub fn summarize_diagnostics<'a, I>(diagnostics: I) -> PackingDiagnosticsSummary +where + I: IntoIterator, +{ + let mut acc = SummaryAccumulator::new(); + for diag in diagnostics { + acc.record(diag); + } + acc.finish() +} + #[cfg(test)] mod tests { use super::*; @@ -1146,4 +1507,107 @@ mod tests { .expect("zweit schwerstes Objekt fehlt"); assert!(second.position.2 <= config.height_epsilon); } + + #[test] + fn footprint_cluster_groups_similar_dimensions() { + let strategy = + FootprintClusterStrategy::new(PackingConfig::DEFAULT_FOOTPRINT_CLUSTER_TOLERANCE); + let mut objects = vec![ + Box3D { + id: 1, + dims: (20.0, 10.0, 10.0), + weight: 30.0, + }, + Box3D { + id: 2, + dims: (20.4, 10.1, 9.5), + weight: 28.0, + }, + Box3D { + id: 3, + dims: (5.0, 5.0, 5.0), + weight: 12.0, + }, + ]; + + objects.sort_by(|a, b| b.weight.partial_cmp(&a.weight).unwrap()); + let reordered = strategy.reorder(objects.clone()); + + assert_eq!(reordered.len(), objects.len()); + assert_eq!(reordered[0].id, 1); + assert_eq!(reordered[1].id, 2); + assert_eq!(reordered[2].id, 3); + } + + #[test] + fn diagnostics_capture_support_and_balance_metrics() { + let config = PackingConfig::default(); + let mut container = Container::new((10.0, 10.0, 30.0), 200.0).unwrap(); + + container.placed.push(PlacedBox { + object: Box3D { + id: 1, + dims: (5.0, 10.0, 10.0), + weight: 8.0, + }, + position: (0.0, 0.0, 0.0), + }); + + container.placed.push(PlacedBox { + object: Box3D { + id: 2, + dims: (10.0, 10.0, 8.0), + weight: 5.0, + }, + position: (0.0, 0.0, 10.0), + }); + + let diagnostics = compute_container_diagnostics(&container, &config); + + assert_eq!(diagnostics.support_samples.len(), 2); + let min_support = diagnostics.minimum_support_percent; + assert!((min_support - 50.0).abs() < 1e-6); + + let avg_support = diagnostics.average_support_percent; + assert!((avg_support - 75.0).abs() < 1e-6); + + assert!(diagnostics.imbalance_ratio > 0.0); + assert!(diagnostics.center_of_mass_offset > 0.0); + + let summary = summarize_diagnostics(std::iter::once(&diagnostics)); + assert!((summary.average_support_percent - 75.0).abs() < 1e-6); + assert!((summary.worst_support_percent - 50.0).abs() < 1e-6); + assert!((summary.max_imbalance_ratio - diagnostics.imbalance_ratio).abs() < 1e-6); + } + + #[test] + fn progress_emits_diagnostics_events() { + let config = PackingConfig::default(); + let objects = vec![ + Box3D { + id: 1, + dims: (10.0, 10.0, 10.0), + weight: 8.0, + }, + Box3D { + id: 2, + dims: (5.0, 5.0, 5.0), + weight: 3.0, + }, + ]; + + let mut diagnostics_events = 0usize; + pack_objects_with_progress( + objects, + single_blueprint((20.0, 20.0, 30.0), 100.0), + config, + |evt| { + if matches!(evt, PackEvent::ContainerDiagnostics { .. }) { + diagnostics_events += 1; + } + }, + ); + + assert!(diagnostics_events >= 1); + } } diff --git a/web/script.js b/web/script.js index 8db7fc3..a8a38d1 100644 --- a/web/script.js +++ b/web/script.js @@ -10,6 +10,7 @@ let liveMode = false; let liveContainers = []; let liveUnplaced = []; let es = null; +let liveDiagnosticsSummary = null; // Konfigurierbare Parameter let config = { @@ -182,6 +183,50 @@ function updateStats(container, dims, visibleCount = null) { : `${liveMode ? 'Live-Container' : 'Container'} ${ currentContainerIndex + 1 }`; + const diagnostics = container.diagnostics ?? null; + const summary = liveMode + ? liveDiagnosticsSummary + : packingResults?.diagnostics_summary ?? null; + + const formatPercent = (value, fractionDigits = 1) => { + if (!Number.isFinite(value)) return '—'; + return `${(value * 100).toFixed(fractionDigits)}%`; + }; + + const formatPlainPercent = (value, fractionDigits = 1) => { + if (!Number.isFinite(value)) return '—'; + return `${value.toFixed(fractionDigits)}%`; + }; + + const limitText = Number.isFinite(diagnostics?.balance_limit) + ? `${diagnostics.balance_limit.toFixed(1)} cm` + : '—'; + const offsetText = Number.isFinite(diagnostics?.center_of_mass_offset) + ? `${diagnostics.center_of_mass_offset.toFixed(1)} cm` + : '—'; + + const diagnosticsHtml = diagnostics + ? ` +

Balance: ${formatPercent( + diagnostics.imbalance_ratio + )} (Limit ${limitText})

+

Schwerpunkt-Abstand: ${offsetText}

+

Unterstützung: Ø ${formatPlainPercent( + diagnostics.average_support_percent + )} · min ${formatPlainPercent(diagnostics.minimum_support_percent)}

+ ` + : ''; + + const summaryHtml = summary + ? ` +
+

Diagnose (gesamt):

+

Max. Ungleichgewicht: ${formatPercent(summary.max_imbalance_ratio)}

+

Unterstützung Ø / min: ${formatPlainPercent( + summary.average_support_percent + )} · ${formatPlainPercent(summary.worst_support_percent)}

+ ` + : ''; document.getElementById('stats').innerHTML = `

${containerTitle} / ${ @@ -203,6 +248,8 @@ function updateStats(container, dims, visibleCount = null) { ? `

Nicht verpackt: ${unplacedCount}

` : '' } + ${diagnosticsHtml} + ${summaryHtml} `; } @@ -575,6 +622,54 @@ function resolveContainerDims(container) { return [50, 50, 50]; } +function recomputeLiveDiagnosticsSummary() { + const diagnosticsList = liveContainers + .map((c) => c.diagnostics) + .filter((diag) => diag && typeof diag === 'object'); + + if (diagnosticsList.length === 0) { + liveDiagnosticsSummary = null; + return; + } + + let maxImbalance = 0; + let worstSupport = 100; + let supportSum = 0; + let supportCount = 0; + + diagnosticsList.forEach((diag) => { + if (Number.isFinite(diag.imbalance_ratio)) { + maxImbalance = Math.max(maxImbalance, diag.imbalance_ratio); + } + if (Number.isFinite(diag.minimum_support_percent)) { + worstSupport = Math.min(worstSupport, diag.minimum_support_percent); + } + const samples = Array.isArray(diag.support_samples) + ? diag.support_samples.filter((sample) => + Number.isFinite(sample?.support_percent) + ) + : []; + + if (samples.length > 0) { + samples.forEach((sample) => { + supportSum += sample.support_percent; + supportCount += 1; + }); + } else if (Number.isFinite(diag.average_support_percent)) { + supportSum += diag.average_support_percent; + supportCount += 1; + } + }); + + const averageSupport = supportCount > 0 ? supportSum / supportCount : 100; + + liveDiagnosticsSummary = { + max_imbalance_ratio: maxImbalance, + worst_support_percent: worstSupport, + average_support_percent: averageSupport, + }; +} + function focusCameraOnDims(dims) { controls.target.set(dims[0] / 2, dims[2] / 2, dims[1] / 2); } @@ -693,6 +788,7 @@ function startLivePacking() { liveMode = true; liveContainers = []; liveUnplaced = []; + liveDiagnosticsSummary = null; currentContainerIndex = 0; updateNavigationButtons(); @@ -761,6 +857,7 @@ function handleLiveEvent(evt) { max_weight: evt.max_weight, label: evt.label ?? null, template_id: evt.template_id ?? null, + diagnostics: null, }); currentContainerIndex = liveContainers.length - 1; visualizeContainer(liveContainers[currentContainerIndex], dims); @@ -779,6 +876,7 @@ function handleLiveEvent(evt) { max_weight: evt.max_weight ?? null, label: evt.label ?? null, template_id: evt.template_id ?? null, + diagnostics: null, }); } const cont = liveContainers[idx]; @@ -796,6 +894,21 @@ function handleLiveEvent(evt) { updateNavigationButtons(); break; } + case 'ContainerDiagnostics': { + const idx = evt.container_id - 1; + const diagnostics = evt.diagnostics ?? null; + if (idx >= 0 && idx < liveContainers.length && diagnostics) { + liveContainers[idx].diagnostics = diagnostics; + recomputeLiveDiagnosticsSummary(); + if (idx === currentContainerIndex) { + updateStats( + liveContainers[idx], + resolveContainerDims(liveContainers[idx]) + ); + } + } + break; + } case 'ObjectRejected': { liveUnplaced.push(evt); console.warn( @@ -814,6 +927,17 @@ function handleLiveEvent(evt) { .join('\n') ); } + if (evt.diagnostics_summary) { + liveDiagnosticsSummary = evt.diagnostics_summary; + } else { + recomputeLiveDiagnosticsSummary(); + } + if (liveContainers.length) { + updateStats( + liveContainers[currentContainerIndex], + resolveContainerDims(liveContainers[currentContainerIndex]) + ); + } break; } } From f5eb4f77d893fb5084ab54e5dfc223eac302318b Mon Sep 17 00:00:00 2001 From: Jonas Pfalzgraf Date: Wed, 29 Oct 2025 18:42:07 +0100 Subject: [PATCH 02/19] =?UTF-8?q?F=C3=BCge=20Diagnosen=20und=20Toleranzen?= =?UTF-8?q?=20zur=20Verpackungsoptimierung=20hinzu=20(#1)=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Füge Utoipa zur API-Dokumentation hinzu und erweitere die Datenmodelle um Schema-Annotationen * 🐛 Füge Unterstützung für bedingte Kompilierung von ungenutzten Imports hinzu und implementiere eine Hilfsfunktion für JSON-Makros * ⚰️ Entferne die nicht verwendete Funktion `json_macro_guard` aus den Dateien `api.rs` und `model.rs` --- Cargo.toml | 1 + src/api.rs | 248 +++++++++++++++++++++++++++++++++++++++-------- src/model.rs | 6 +- src/optimizer.rs | 7 +- 4 files changed, 217 insertions(+), 45 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bb36a23..7122f37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ tar = "0.4" zip = { version = "0.6", default-features = false, features = ["deflate"] } sha2 = "0.10" dotenvy = "0.15" +utoipa = { version = "5.4", features = ["axum_extras"] } [target.'cfg(windows)'.dependencies] winreg = "0.52" diff --git a/src/api.rs b/src/api.rs index 6e7ed2d..d5eafbe 100644 --- a/src/api.rs +++ b/src/api.rs @@ -3,6 +3,7 @@ //! Bietet HTTP-Endpunkte zur Kommunikation mit dem Frontend. //! Verwendet Axum als Web-Framework und unterstützt CORS. +use axum::extract::rejection::JsonRejection; use axum::extract::{Json, State}; use axum::response::sse::{Event, KeepAlive, Sse}; use axum::{ @@ -13,16 +14,20 @@ use axum::{ }; use rust_embed::RustEmbed; use serde::{Deserialize, Serialize}; +#[cfg_attr(not(test), allow(unused_imports))] +use serde_json::json; +use std::sync::OnceLock; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; use tokio_stream::StreamExt; use tower_http::cors::{Any, CorsLayer}; +use utoipa::{OpenApi, ToSchema}; use crate::config::{ApiConfig, OptimizerConfig}; use crate::model::{Box3D, ContainerBlueprint, ValidationError}; use crate::optimizer::{ pack_objects_with_config, pack_objects_with_progress, ContainerDiagnostics, - PackingDiagnosticsSummary, PackingResult, + PackingDiagnosticsSummary, PackingResult, SupportDiagnostics, }; #[derive(Clone)] @@ -30,6 +35,50 @@ struct ApiState { optimizer_config: OptimizerConfig, } +static OPENAPI_DOC: OnceLock = OnceLock::new(); + +const SWAGGER_UI_HTML: &str = r##" + + + + sort-it-now API Docs + + + +
+ + + + + "##; + +fn openapi_doc() -> &'static utoipa::openapi::OpenApi { + OPENAPI_DOC.get_or_init(ApiDoc::openapi) +} + /// Embedded Web Assets (HTML, CSS, JS) #[derive(RustEmbed)] #[folder = "web/"] @@ -38,9 +87,10 @@ struct WebAssets; /// Request-Struktur für den Packing-Endpunkt. /// /// `containers` enthält die möglichen Verpackungstypen, die kombiniert werden dürfen. -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, ToSchema)] pub struct ContainerRequest { pub name: Option, + #[schema(value_type = [f64; 3], example = json!([120.0, 100.0, 80.0]))] pub dims: (f64, f64, f64), pub max_weight: f64, } @@ -51,7 +101,21 @@ impl ContainerRequest { } } -#[derive(Deserialize)] +#[derive(Deserialize, ToSchema)] +#[schema( + example = json!({ + "containers": [ + { + "name": "Standardkiste", + "dims": [120.0, 100.0, 80.0], + "max_weight": 500.0 + } + ], + "objects": [ + { "id": 1, "dims": [30.0, 40.0, 20.0], "weight": 5.0 } + ] + }) +)] pub struct PackRequest { pub containers: Vec, pub objects: Vec, @@ -84,7 +148,7 @@ impl PackRequest { /// /// # Felder /// * `results` - Vector von Containern mit platzierten Objekten -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct PackResponse { pub results: Vec, pub unplaced: Vec, @@ -98,11 +162,12 @@ pub struct PackResponse { /// * `id` - Container-Nummer (1-basiert) /// * `total_weight` - Gesamtgewicht aller Objekte im Container /// * `placed` - Liste der platzierten Objekte mit Positionen -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct PackedContainer { pub id: usize, pub template_id: Option, pub label: Option, + #[schema(value_type = [f64; 3], example = json!([120.0, 100.0, 80.0]))] pub dims: (f64, f64, f64), pub max_weight: f64, pub total_weight: f64, @@ -117,23 +182,73 @@ pub struct PackedContainer { /// * `pos` - Position (x, y, z) im Container /// * `weight` - Gewicht in kg /// * `dims` - Dimensionen (Breite, Tiefe, Höhe) -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct PackedObject { pub id: usize, + #[schema(value_type = [f64; 3], example = json!([0.0, 0.0, 0.0]))] pub pos: (f64, f64, f64), pub weight: f64, + #[schema(value_type = [f64; 3], example = json!([30.0, 40.0, 20.0]))] pub dims: (f64, f64, f64), } -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct PackedUnplacedObject { pub id: usize, pub weight: f64, + #[schema(value_type = [f64; 3], example = json!([35.0, 45.0, 25.0]))] pub dims: (f64, f64, f64), pub reason_code: String, pub reason: String, } +#[derive(Serialize, ToSchema)] +struct ErrorResponse { + error: String, + details: String, +} + +impl ErrorResponse { + fn new(error: impl Into, details: impl Into) -> Self { + Self { + error: error.into(), + details: details.into(), + } + } +} + +fn error_response( + status: StatusCode, + error: impl Into, + details: impl Into, +) -> Response { + (status, Json(ErrorResponse::new(error, details))).into_response() +} + +fn json_deserialize_error(err: JsonRejection) -> Response { + error_response( + StatusCode::UNPROCESSABLE_ENTITY, + "Ungültige JSON-Daten", + err.to_string(), + ) +} + +fn validation_error(details: impl Into) -> Response { + error_response( + StatusCode::UNPROCESSABLE_ENTITY, + "Ungültige Eingabedaten", + details, + ) +} + +fn container_config_error(details: impl Into) -> Response { + error_response( + StatusCode::UNPROCESSABLE_ENTITY, + "Ungültige Container-Konfiguration", + details, + ) +} + impl PackResponse { /// Erstellt eine PackResponse aus einem PackingResult (DRY-Prinzip). pub fn from_packing_result(result: PackingResult) -> Self { @@ -193,6 +308,28 @@ impl PackResponse { } } +#[derive(OpenApi)] +#[openapi( + paths(handle_pack, handle_pack_stream), + components( + schemas( + PackRequest, + ContainerRequest, + PackResponse, + PackedContainer, + PackedObject, + PackedUnplacedObject, + ErrorResponse, + Box3D, + ContainerDiagnostics, + SupportDiagnostics, + PackingDiagnosticsSummary + ) + ), + tags((name = "packing", description = "Endpunkte zur Verpackungsoptimierung")) +)] +struct ApiDoc; + /// Startet den API-Server auf Port 8080. /// /// Konfiguriert CORS für Cross-Origin-Requests vom Frontend. @@ -209,6 +346,9 @@ pub async fn start_api_server(config: ApiConfig, optimizer_config: OptimizerConf // API-Endpunkte .route("/pack", post(handle_pack)) .route("/pack_stream", post(handle_pack_stream)) + // API-Dokumentation + .route("/docs/openapi.json", get(serve_openapi_json)) + .route("/docs", get(serve_openapi_ui)) // Web-UI (embedded) .route("/", get(serve_index)) .route("/*path", get(serve_static)) @@ -235,6 +375,9 @@ pub async fn start_api_server(config: ApiConfig, optimizer_config: OptimizerConf println!("📦 API-Endpunkte:"); println!(" - POST /pack"); println!(" - POST /pack_stream"); + println!("📑 Dokumentation:"); + println!(" - GET /docs"); + println!(" - GET /docs/openapi.json"); println!("🌐 Web-UI: http://{}:{}", display_host, config.port()); if let Err(err) = axum::serve(listener, app).await { @@ -251,20 +394,32 @@ pub async fn start_api_server(config: ApiConfig, optimizer_config: OptimizerConf /// /// # Rückgabewert /// JSON-Response mit allen benötigten Containern und platzierten Objekten +#[utoipa::path( + post, + path = "/pack", + request_body = PackRequest, + responses( + (status = 200, description = "Erfolgreiche Verpackung der Objekte", body = PackResponse), + ( + status = UNPROCESSABLE_ENTITY, + description = "Ungültige Anfrage oder Container-Konfiguration", + body = ErrorResponse + ) + ), + tag = "packing" +)] async fn handle_pack( State(state): State, - Json(payload): Json, + payload: Result, JsonRejection>, ) -> impl IntoResponse { + let Json(payload) = match payload { + Ok(payload) => payload, + Err(err) => return json_deserialize_error(err), + }; + // Validiere Eingabedaten if let Err(e) = payload.validate() { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "error": "Ungültige Eingabedaten", - "details": e.to_string() - })), - ) - .into_response(); + return validation_error(e.to_string()); } let PackRequest { @@ -279,14 +434,7 @@ async fn handle_pack( { Ok(list) => list, Err(e) => { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "error": "Ungültige Container-Konfiguration", - "details": e.to_string() - })), - ) - .into_response(); + return container_config_error(e.to_string()); } }; @@ -311,20 +459,37 @@ async fn handle_pack( /// /// Streamt die Pack-Events in Echtzeit als Server-Sent Events (text/event-stream). /// Das Frontend kann die Schritte live visualisieren, ohne auf das Gesamtergebnis zu warten. +#[utoipa::path( + post, + path = "/pack_stream", + request_body = PackRequest, + responses( + ( + status = 200, + description = "Streamt Pack-Events in Echtzeit", + content_type = "text/event-stream", + body = String + ), + ( + status = UNPROCESSABLE_ENTITY, + description = "Ungültige Anfrage oder Container-Konfiguration", + body = ErrorResponse + ) + ), + tag = "packing" +)] async fn handle_pack_stream( State(state): State, - Json(payload): Json, + payload: Result, JsonRejection>, ) -> impl IntoResponse { + let Json(payload) = match payload { + Ok(payload) => payload, + Err(err) => return json_deserialize_error(err), + }; + // Validiere Eingabedaten if let Err(e) = payload.validate() { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "error": "Ungültige Eingabedaten", - "details": e.to_string() - })), - ) - .into_response(); + return validation_error(e.to_string()); } let PackRequest { @@ -339,14 +504,7 @@ async fn handle_pack_stream( { Ok(list) => list, Err(e) => { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "error": "Ungültige Container-Konfiguration", - "details": e.to_string() - })), - ) - .into_response(); + return container_config_error(e.to_string()); } }; @@ -396,3 +554,11 @@ async fn serve_static(uri: Uri) -> Response { None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(), } } + +async fn serve_openapi_json(State(_state): State) -> impl IntoResponse { + Json(openapi_doc()) +} + +async fn serve_openapi_ui(State(_state): State) -> impl IntoResponse { + Html(SWAGGER_UI_HTML) +} diff --git a/src/model.rs b/src/model.rs index a94e893..ff145c6 100644 --- a/src/model.rs +++ b/src/model.rs @@ -6,6 +6,9 @@ //! - `Container`: Der Verpackungsbehälter mit Kapazitätsgrenzen use serde::{Deserialize, Serialize}; +#[cfg_attr(not(test), allow(unused_imports))] +use serde_json::json; +use utoipa::ToSchema; /// Validierungsfehler für Objektdaten. #[derive(Debug, Clone)] @@ -35,9 +38,10 @@ impl std::error::Error for ValidationError {} /// * `id` - Eindeutige Identifikationsnummer des Objekts /// * `dims` - Dimensionen (Breite, Tiefe, Höhe) in Einheiten /// * `weight` - Gewicht des Objekts in kg -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct Box3D { pub id: usize, + #[schema(value_type = [f64; 3], example = json!([30.0, 40.0, 20.0]))] pub dims: (f64, f64, f64), pub weight: f64, } diff --git a/src/optimizer.rs b/src/optimizer.rs index 572de31..4512151 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -11,6 +11,7 @@ use std::cmp::Ordering; use crate::geometry::{intersects, overlap_1d, point_inside}; use crate::model::{Box3D, Container, ContainerBlueprint, PlacedBox}; +use utoipa::ToSchema; /// Konfiguration für den Packing-Algorithmus. /// @@ -205,7 +206,7 @@ impl ObjectCluster { } /// Support-Kennzahlen pro Objekt. -#[derive(Clone, Debug, serde::Serialize)] +#[derive(Clone, Debug, serde::Serialize, ToSchema)] pub struct SupportDiagnostics { pub object_id: usize, pub support_percent: f64, @@ -213,7 +214,7 @@ pub struct SupportDiagnostics { } /// Diagnostische Kennzahlen pro Container für Monitoring. -#[derive(Clone, Debug, serde::Serialize)] +#[derive(Clone, Debug, serde::Serialize, ToSchema)] pub struct ContainerDiagnostics { pub center_of_mass_offset: f64, pub balance_limit: f64, @@ -224,7 +225,7 @@ pub struct ContainerDiagnostics { } /// Zusammenfassung wichtiger Kennzahlen über alle Container hinweg. -#[derive(Clone, Debug, serde::Serialize)] +#[derive(Clone, Debug, serde::Serialize, ToSchema)] pub struct PackingDiagnosticsSummary { pub max_imbalance_ratio: f64, pub worst_support_percent: f64, From c0e30e5d5139a82363f53e6760a3dcd29523ad82 Mon Sep 17 00:00:00 2001 From: Jonas Pfalzgraf Date: Wed, 29 Oct 2025 19:43:01 +0100 Subject: [PATCH 03/19] Vorbereitung zu Version 1.0.0 (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Aktualisiere die Konfiguration und den Code für die 3D-Verpackungsoptimierung: - Füge OpenAPI-Dokumentation und Swagger UI hinzu - Aktualisiere die Version und Lizenz in den Konfigurationsdateien - Verbessere die Validierung von Packanfragen im API-Handler - Optimiere die Umgebungsvariablenverarbeitung in der Konfiguration - Bereinige den Code und entferne ungenutzte Importe * Update src/api.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update package.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/rust.yml | 32 ++++-- Cargo.toml | 5 +- LICENSE | 174 ++++++++++++++++++++++++++++++ README.md | 18 ++-- package.json | 4 +- src/api.rs | 216 +++++++++++++++++++++++-------------- src/config.rs | 23 ++-- src/model.rs | 2 +- src/update.rs | 7 +- 9 files changed, 366 insertions(+), 115 deletions(-) create mode 100644 LICENSE diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9fd45e0..1516ff3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,21 +2,35 @@ name: Rust on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] env: CARGO_TERM_COLOR: always jobs: build: - - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@v4 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo build outputs + uses: Swatinem/rust-cache@v2 + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Lint with clippy + run: cargo clippy --workspace --all-targets + + - name: Run tests + run: cargo test --workspace --all-features --verbose diff --git a/Cargo.toml b/Cargo.toml index 7122f37..3986c70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "sort_it_now" -version = "0.1.0" -edition = "2021" +version = "1.0.0" +edition = "2024" +license = "NCSL-1.0" [dependencies] axum = "0.7" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e9d0711 --- /dev/null +++ b/LICENSE @@ -0,0 +1,174 @@ +# Non-Commercial Source License (NCSL) v1.0 + +Copyright (c) 2025 Jonas Pfalzgraf / JosunLP + +This license allows non-commercial use of the Software. Any commercial use +requires the Licensor’s prior written permission. This is a source-available +license and is not an OSI-approved open-source license. + +## 1. Definitions + +1.1 “Software” means the source code, object code, scripts, configuration files, +documentation, and any other materials included in this repository or otherwise +distributed with this license. + +1.2 “Use” means to run, reproduce, display, test, modify, create derivative +works of, merge, distribute, or make available as a service. + +1.3 “Non-Commercial Purposes” are purposes that are not intended to obtain a +commercial advantage or monetary compensation, whether direct or indirect. +Examples include research, teaching, personal/hobby projects, and internal +evaluation. Mere cost recovery without a profit motive is treated as +non-commercial. + +1.4 “Commercial Use” includes, without limitation: offering paid products or +services; use within for-profit business models; providing or incorporating the +Software into paid, ad-supported, or monetized offerings; use to deliver paid +services or consulting; or use in production systems of an entity pursuing +profit. In cases of doubt, the use is deemed Commercial unless the Licensor +provides written permission. + +1.5 “Licensor” means Jonas Pfalzgraf. “Licensee” means any person or entity that +uses the Software. + +## 2. Grant of Rights (Non-Commercial Only) + +2.1 Subject to this License, the Licensor grants the Licensee a worldwide, +royalty-free, non-exclusive, non-transferable, and non-sublicensable license to +Use the Software solely for Non-Commercial Purposes. + +2.2 For Non-Commercial Purposes, the Licensee may copy, run, modify, create +derivatives of, and distribute the Software, provided that: + +- (a) this License text is included with each copy and derivative work, +- (b) clear copyright and license notices are retained, and +- (c) modifications are clearly marked. + +## 3. Prohibition of Commercial Use Without Permission + +3.1 Any Commercial Use is prohibited without the Licensor’s prior, express, +written permission. + +3.2 For permissions and commercial licenses, contact: [Contact Email/URL]. + +3.3 Unauthorized Commercial Use may result in injunctive relief and damages to +the maximum extent permitted by law. + +## 4. Offering as a Service (SaaS/Cloud) + +4.1 Making the Software or derivative works available to third parties as a +service constitutes Use. Such Use is permitted for Non-Commercial Purposes only +and is prohibited for Commercial Use absent written permission under Section 3. + +## 5. Source-Available; Not Open Source + +5.1 This License permits access to and Use of the source code but is not +compliant with the Open Source Definition of the OSI because it restricts +Commercial Use. + +## 6. Patents + +6.1 To the extent the Licensor owns enforceable patent claims necessarily +infringed by Non-Commercial Use of the Software, the Licensor grants a limited, +royalty-free patent license for such Non-Commercial Use. No patent license is +granted for Commercial Use. + +6.2 Any breach of this License automatically and retroactively terminates the +patent license granted under 6.1. + +## 7. Trademarks + +7.1 This License does not grant any rights to use the Licensor’s names, +trademarks, or logos, except for factual attribution. + +## 8. No Warranty; Disclaimer of Liability + +8.1 The Software is provided “AS IS” and “AS AVAILABLE,” with all faults and +without warranties of any kind, whether express, implied, statutory, or +otherwise, including, without limitation, warranties of merchantability, fitness +for a particular purpose, title, non-infringement, quiet enjoyment, or that the +Software will be error-free, uninterrupted, secure, or meet the Licensee’s +requirements. + +8.2 To the fullest extent permitted by applicable law, the Licensor disclaims +all responsibility and liability for any and all claims, losses, damages, +costs, or expenses of any kind, whether direct, indirect, incidental, special, +consequential, exemplary, or punitive, arising out of or in connection with the +Software or this License, including but not limited to loss of profits, revenue, +data, goodwill, business interruption, procurement of substitute goods or +services, failure of security mechanisms, or any other commercial or economic +loss, in each case whether based on contract, tort (including negligence), +strict liability, or any other theory, even if advised of the possibility of +such damages and even if a remedy fails of its essential purpose. + +8.3 The Licensee assumes all risks and responsibilities for selection, Use, and +results obtained from the Software. + +8.4 Some jurisdictions do not allow the exclusion or limitation of certain +warranties or liabilities; in such cases, the exclusions and limitations above +apply to the maximum extent permitted by law. + +## 9. Compliance; Legal Requirements + +9.1 The Licensee is responsible for complying with all applicable laws and +regulations, including export controls, data protection, and security +requirements. + +9.2 The Licensee shall not circumvent or attempt to circumvent any technical +limitations or usage restrictions related to the Software. + +## 10. Term and Termination + +10.1 This License is effective upon Use of the Software and continues until +terminated. + +10.2 Upon any breach of this License, all rights granted hereunder terminate +automatically without notice. Upon termination, all Use and distribution must +cease immediately; archival retention for evidentiary purposes is permitted. + +10.3 The Licensor may terminate this License for cause with immediate effect +with respect to a breaching Licensee, to the extent permitted by law. + +## 11. Commercial Licenses; Exceptions + +11.1 The Licensor may offer separate commercial licenses granting additional or +different rights. Such licenses must be in writing and signed by the Licensor. + +11.2 Contributions by third parties are, by default, licensed under this same +License unless otherwise agreed in writing (e.g., via a Contributor License +Agreement). + +## 12. Notices and Attribution + +12.1 The Licensee must retain all copyright, license, and attribution notices in +the Software and accompanying documentation in any significant portions or +derivative works. + +## 13. Severability; Governing Law; Venue + +13.1 If any provision of this License is held invalid or unenforceable, the +remaining provisions shall remain in full force and effect, and the invalid +provision shall be replaced by a valid provision that most closely reflects the +original intent and economic purpose. + +13.2 This License is governed by the laws of Germany. Mandatory consumer protections of the Licensee’s home +jurisdiction remain unaffected where required by law. + +13.3 Exclusive venue for disputes shall be the courts of Hamburg, Germany, +to the extent permitted by law. + +## 14. Entire Agreement; Amendments + +14.1 This License constitutes the entire agreement between the parties with +respect to the Software. No oral agreements exist. + +14.2 Amendments must be in a signed writing by the Licensor. + +## 15. Summary (Non-Binding) + +- Non-commercial use: permitted. +- Commercial use: only with the Licensor’s written permission. +- View, modify, share: yes, for non-commercial purposes with notices. +- No warranty; Licensor disclaims liability to the maximum extent allowed. + +Contact: diff --git a/README.md b/README.md index a42eef2..434fd30 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Eine intelligente 3D-Verpackungsoptimierung mit interaktiver Visualisierung. - **Automatische Multi-Container-Verwaltung** - **Umfassende Unit-Tests** - **REST-API** mit JSON-Kommunikation +- **OpenAPI & Swagger UI** mit live Dokumentation unter `/docs` - **OOP-Prinzipien** mit DRY-Architektur - **Vollständig dokumentierter Code** (Rust-Docstrings) @@ -50,17 +51,7 @@ Der Server läuft auf `http://localhost:8080` ### Frontend öffnen -Öffne `web/index.html` in einem Browser oder starte einen lokalen Webserver: - -```bash -# Python 3 -python3 -m http.server 8000 --directory web - -# Node.js -npx http-server web -p 8000 -``` - -Dann öffne: `http://localhost:8000` +Der Web-Client wird automatisch vom Rust-Backend ausgeliefert. Rufe nach dem Start einfach `http://localhost:8080/` im Browser auf. Im Browser: @@ -93,6 +84,11 @@ Beim Start prüft der Dienst im Hintergrund die neuesten GitHub-Releases (`Josun ## 📊 API-Endpunkte +### OpenAPI & Swagger UI + +- `GET /docs` liefert eine interaktive Swagger UI mit Subresource-Integrity-geschützten Assets. +- `GET /docs/openapi.json` stellt das OpenAPI-Schema (v3) bereit und kann z. B. für Code-Generatoren genutzt werden. + ### POST /pack Verpackt Objekte in Container. diff --git a/package.json b/package.json index d843542..8f44b8b 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "sort-it-now", - "version": "0.1.0", + "version": "1.0.0", "description": "", - "license": "MIT", + "license": "LicenseRef-NCSL-1.0", "author": "", "scripts": { "test": "cargo test", diff --git a/src/api.rs b/src/api.rs index d5eafbe..945c9b8 100644 --- a/src/api.rs +++ b/src/api.rs @@ -7,27 +7,27 @@ use axum::extract::rejection::JsonRejection; use axum::extract::{Json, State}; use axum::response::sse::{Event, KeepAlive, Sse}; use axum::{ - http::{header, StatusCode, Uri}, + Router, + http::{StatusCode, Uri, header}, response::{Html, IntoResponse, Response}, routing::{get, post}, - Router, }; use rust_embed::RustEmbed; use serde::{Deserialize, Serialize}; -#[cfg_attr(not(test), allow(unused_imports))] +#[allow(unused_imports)] use serde_json::json; use std::sync::OnceLock; use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; use tokio_stream::StreamExt; +use tokio_stream::wrappers::ReceiverStream; use tower_http::cors::{Any, CorsLayer}; use utoipa::{OpenApi, ToSchema}; use crate::config::{ApiConfig, OptimizerConfig}; -use crate::model::{Box3D, ContainerBlueprint, ValidationError}; +use crate::model::{Box3D, Container, ContainerBlueprint, ValidationError}; use crate::optimizer::{ - pack_objects_with_config, pack_objects_with_progress, ContainerDiagnostics, - PackingDiagnosticsSummary, PackingResult, SupportDiagnostics, + ContainerDiagnostics, PackingDiagnosticsSummary, PackingResult, SupportDiagnostics, + pack_objects_with_config, pack_objects_with_progress, }; #[derive(Clone)] @@ -121,26 +121,58 @@ pub struct PackRequest { pub objects: Vec, } -impl PackRequest { - /// Validiert die Request-Daten. - pub fn validate(&self) -> Result<(), ValidationError> { - if self.containers.is_empty() { - return Err(ValidationError::InvalidConfiguration( - "Mindestens ein Verpackungstyp muss angegeben werden".to_string(), - )); - } +#[derive(Debug)] +struct ValidatedPackRequest { + containers: Vec, + objects: Vec, +} - // Validiere Container-Typen - for (idx, cont) in self.containers.iter().enumerate() { - ContainerBlueprint::new(idx, cont.name.clone(), cont.dims, cont.max_weight)?; - } +impl ValidatedPackRequest { + fn container_count(&self) -> usize { + self.containers.len() + } + + fn object_count(&self) -> usize { + self.objects.len() + } - // Validiere alle Objekte - for obj in &self.objects { - Box3D::new(obj.id, obj.dims, obj.weight)?; + fn into_parts(self) -> (Vec, Vec) { + (self.objects, self.containers) + } +} + +#[derive(Debug)] +enum PackRequestValidationError { + MissingContainers, + InvalidContainer(ValidationError), + InvalidObject(ValidationError), +} + +impl PackRequest { + fn into_validated(self) -> Result { + if self.containers.is_empty() { + return Err(PackRequestValidationError::MissingContainers); } - Ok(()) + let containers = self + .containers + .into_iter() + .enumerate() + .map(|(idx, spec)| spec.into_blueprint(idx)) + .collect::, ValidationError>>() + .map_err(PackRequestValidationError::InvalidContainer)?; + + let objects = self + .objects + .into_iter() + .map(|obj| Box3D::new(obj.id, obj.dims, obj.weight)) + .collect::, ValidationError>>() + .map_err(PackRequestValidationError::InvalidObject)?; + + Ok(ValidatedPackRequest { + containers, + objects, + }) } } @@ -249,6 +281,28 @@ fn container_config_error(details: impl Into) -> Response { ) } +fn parse_pack_request( + payload: Result, JsonRejection>, +) -> Result { + let Json(payload) = match payload { + Ok(payload) => payload, + Err(err) => return Err(json_deserialize_error(err)), + }; + + match payload.into_validated() { + Ok(validated) => Ok(validated), + Err(PackRequestValidationError::MissingContainers) => Err(validation_error( + "Mindestens ein Verpackungstyp muss angegeben werden", + )), + Err(PackRequestValidationError::InvalidContainer(err)) => { + Err(container_config_error(err.to_string())) + } + Err(PackRequestValidationError::InvalidObject(err)) => { + Err(validation_error(err.to_string())) + } + } +} + impl PackResponse { /// Erstellt eine PackResponse aus einem PackingResult (DRY-Prinzip). pub fn from_packing_result(result: PackingResult) -> Self { @@ -268,9 +322,16 @@ impl PackResponse { .zip(container_diagnostics.into_iter()) .enumerate() .map(|(i, (cont, diagnostics))| { - let total_weight = cont.total_weight(); - let placed_objects = cont - .placed + let Container { + dims, + max_weight, + placed, + template_id, + label, + } = cont; + + let total_weight = placed.iter().map(|b| b.object.weight).sum(); + let placed_objects = placed .into_iter() .map(|p| PackedObject { id: p.object.id, @@ -282,10 +343,10 @@ impl PackResponse { PackedContainer { id: i + 1, - template_id: cont.template_id, - label: cont.label.clone(), - dims: cont.dims, - max_weight: cont.max_weight, + template_id, + label, + dims, + max_weight, total_weight, placed: placed_objects, diagnostics, @@ -412,36 +473,18 @@ async fn handle_pack( State(state): State, payload: Result, JsonRejection>, ) -> impl IntoResponse { - let Json(payload) = match payload { - Ok(payload) => payload, - Err(err) => return json_deserialize_error(err), + let request = match parse_pack_request(payload) { + Ok(request) => request, + Err(response) => return response, }; - // Validiere Eingabedaten - if let Err(e) = payload.validate() { - return validation_error(e.to_string()); - } - - let PackRequest { - containers, - objects, - } = payload; - let container_blueprints = match containers - .into_iter() - .enumerate() - .map(|(idx, spec)| spec.into_blueprint(idx)) - .collect::, ValidationError>>() - { - Ok(list) => list, - Err(e) => { - return container_config_error(e.to_string()); - } - }; + let object_count = request.object_count(); + let container_count = request.container_count(); + let (objects, container_blueprints) = request.into_parts(); println!( "📥 Neue Pack-Anfrage: {} Objekte, {} Verpackungstypen", - objects.len(), - container_blueprints.len() + object_count, container_count ); let packing_config = state.optimizer_config.packing_config(); let packing_result = pack_objects_with_config(objects, container_blueprints, packing_config); @@ -482,31 +525,12 @@ async fn handle_pack_stream( State(state): State, payload: Result, JsonRejection>, ) -> impl IntoResponse { - let Json(payload) = match payload { - Ok(payload) => payload, - Err(err) => return json_deserialize_error(err), + let request = match parse_pack_request(payload) { + Ok(request) => request, + Err(response) => return response, }; - // Validiere Eingabedaten - if let Err(e) = payload.validate() { - return validation_error(e.to_string()); - } - - let PackRequest { - containers, - objects, - } = payload; - let container_blueprints = match containers - .into_iter() - .enumerate() - .map(|(idx, spec)| spec.into_blueprint(idx)) - .collect::, ValidationError>>() - { - Ok(list) => list, - Err(e) => { - return container_config_error(e.to_string()); - } - }; + let (objects, container_blueprints) = request.into_parts(); let (tx, rx) = mpsc::channel::(32); @@ -562,3 +586,39 @@ async fn serve_openapi_json(State(_state): State) -> impl IntoResponse async fn serve_openapi_ui(State(_state): State) -> impl IntoResponse { Html(SWAGGER_UI_HTML) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn openapi_doc_lists_expected_paths() { + let doc = openapi_doc(); + let paths = &doc.paths.paths; + assert!( + paths.contains_key("/pack"), + "OpenAPI-Dokumentation fehlt der /pack Pfad" + ); + assert!( + paths.contains_key("/pack_stream"), + "OpenAPI-Dokumentation fehlt der /pack_stream Pfad" + ); + } + + #[test] + fn openapi_doc_contains_key_schemas() { + let doc = openapi_doc(); + let components = doc + .components + .as_ref() + .expect("OpenAPI-Dokumentation enthält keine Components"); + let schemas = &components.schemas; + for name in ["PackRequest", "PackResponse", "ErrorResponse"] { + assert!( + schemas.contains_key(name), + "Erwartetes Schema '{}' fehlt im OpenAPI-Spec", + name + ); + } + } +} diff --git a/src/config.rs b/src/config.rs index 329b580..fe50bc3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -169,9 +169,7 @@ impl OptimizerConfig { const FOOTPRINT_TOLERANCE_VAR: &'static str = "SORT_IT_NOW_PACKING_FOOTPRINT_TOLERANCE"; fn from_env() -> Self { - let mut packing = PackingConfig::default(); - - packing.grid_step = load_f64_with_warning( + let grid_step = load_f64_with_warning( Self::GRID_STEP_VAR, PackingConfig::DEFAULT_GRID_STEP, |value| value > 0.0, @@ -179,7 +177,7 @@ impl OptimizerConfig { "Warnung: Angepasste Raster-Schrittweite kann die Pack-Stabilität beeinträchtigen", ); - packing.support_ratio = load_f64_with_warning( + let support_ratio = load_f64_with_warning( Self::SUPPORT_RATIO_VAR, PackingConfig::DEFAULT_SUPPORT_RATIO, |value| (0.0..=1.0).contains(&value), @@ -187,7 +185,7 @@ impl OptimizerConfig { "Warnung: Angepasste Mindestauflage kann zu instabilen Stapeln führen", ); - packing.height_epsilon = load_f64_with_warning( + let height_epsilon = load_f64_with_warning( Self::HEIGHT_EPSILON_VAR, PackingConfig::DEFAULT_HEIGHT_EPSILON, |value| value > 0.0, @@ -195,7 +193,7 @@ impl OptimizerConfig { "Warnung: Angepasste Höhen-Toleranz kann unerwartete Platzierungen verursachen", ); - packing.general_epsilon = load_f64_with_warning( + let general_epsilon = load_f64_with_warning( Self::GENERAL_EPSILON_VAR, PackingConfig::DEFAULT_GENERAL_EPSILON, |value| value > 0.0, @@ -203,7 +201,7 @@ impl OptimizerConfig { "Warnung: Angepasste Toleranzen können numerische Instabilitäten hervorrufen", ); - packing.balance_limit_ratio = load_f64_with_warning( + let balance_limit_ratio = load_f64_with_warning( Self::BALANCE_RATIO_VAR, PackingConfig::DEFAULT_BALANCE_LIMIT_RATIO, |value| (0.0..=1.0).contains(&value), @@ -211,7 +209,7 @@ impl OptimizerConfig { "Warnung: Angepasste Balance-Grenzen können zum Umkippen von Stapeln führen", ); - packing.footprint_cluster_tolerance = load_f64_with_warning( + let footprint_cluster_tolerance = load_f64_with_warning( Self::FOOTPRINT_TOLERANCE_VAR, PackingConfig::DEFAULT_FOOTPRINT_CLUSTER_TOLERANCE, |value| (0.0..=0.5).contains(&value), @@ -219,6 +217,15 @@ impl OptimizerConfig { "Warnung: Angepasste Footprint-Gruppierung kann zu unerwarteten Platzierungen führen", ); + let packing = PackingConfig::builder() + .grid_step(grid_step) + .support_ratio(support_ratio) + .height_epsilon(height_epsilon) + .general_epsilon(general_epsilon) + .balance_limit_ratio(balance_limit_ratio) + .footprint_cluster_tolerance(footprint_cluster_tolerance) + .build(); + Self { packing } } diff --git a/src/model.rs b/src/model.rs index ff145c6..8bb1d43 100644 --- a/src/model.rs +++ b/src/model.rs @@ -6,7 +6,7 @@ //! - `Container`: Der Verpackungsbehälter mit Kapazitätsgrenzen use serde::{Deserialize, Serialize}; -#[cfg_attr(not(test), allow(unused_imports))] +#[allow(unused_imports)] use serde_json::json; use utoipa::ToSchema; diff --git a/src/update.rs b/src/update.rs index 819752f..d31d803 100644 --- a/src/update.rs +++ b/src/update.rs @@ -1,5 +1,5 @@ -use reqwest::header::HeaderMap; use reqwest::StatusCode; +use reqwest::header::HeaderMap; use serde::Deserialize; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -467,8 +467,7 @@ fn http_timeout() -> Duration { } else { eprintln!( "⚠️ Konnte SORT_IT_NOW_HTTP_TIMEOUT_SECS ('{}') nicht parsen. Verwende Standardtimeout {}s.", - trimmed, - DEFAULT_TIMEOUT_SECS + trimmed, DEFAULT_TIMEOUT_SECS ); Duration::from_secs(DEFAULT_TIMEOUT_SECS) } @@ -741,8 +740,8 @@ async fn copy_readme_if_present(bundle_dir: &Path, install_dir: &Path) { #[cfg(target_os = "windows")] fn ensure_windows_path(install_dir: &Path) -> Result { - use winreg::enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE}; use winreg::RegKey; + use winreg::enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE}; let hkcu = RegKey::predef(HKEY_CURRENT_USER); let env = hkcu.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?; From 2852e8843a8a38291a511b286bef0909e997a4ca Mon Sep 17 00:00:00 2001 From: Jonas Pfalzgraf Date: Wed, 29 Oct 2025 20:33:37 +0100 Subject: [PATCH 04/19] Update Cargo.toml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3986c70..033a363 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "sort_it_now" version = "1.0.0" edition = "2024" -license = "NCSL-1.0" +license = "LicenseRef-NCSL-1.0" [dependencies] axum = "0.7" From caedf4a3003222dcc06e33f88b81eaa681357660 Mon Sep 17 00:00:00 2001 From: JonasPfalzgraf <20913954+JosunLP@users.noreply.github.com> Date: Wed, 29 Oct 2025 20:40:48 +0100 Subject: [PATCH 05/19] =?UTF-8?q?=F0=9F=90=9B=20Verbessere=20die=20Berechn?= =?UTF-8?q?ung=20des=20Unterst=C3=BCtzungsprozentsatzes=20in=20der=20Zusam?= =?UTF-8?q?menfassungsakkumulation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/optimizer.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/optimizer.rs b/src/optimizer.rs index 4512151..81ad2fc 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -1171,7 +1171,12 @@ impl SummaryAccumulator { let sample_count = diagnostics.support_samples.len(); if sample_count > 0 { - self.support_percent_sum += diagnostics.average_support_percent * sample_count as f64; + let support_sum: f64 = diagnostics + .support_samples + .iter() + .map(|sample| sample.support_percent) + .sum(); + self.support_percent_sum += support_sum; self.support_sample_count += sample_count; } } From 1282259f63f2d61c84ba31545f632ee9be9df021 Mon Sep 17 00:00:00 2001 From: JonasPfalzgraf <20913954+JosunLP@users.noreply.github.com> Date: Wed, 29 Oct 2025 20:43:37 +0100 Subject: [PATCH 06/19] =?UTF-8?q?=F0=9F=90=9B=20=C3=84ndere=20Debug-Assert?= =?UTF-8?q?ion=20in=20Panic=20f=C3=BCr=20Synchronisationsfehler=20im=20Dia?= =?UTF-8?q?gnostikvektor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/optimizer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/optimizer.rs b/src/optimizer.rs index 81ad2fc..f0c07a3 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -562,8 +562,7 @@ pub fn pack_objects_with_progress( } else if idx == container_diagnostics.len() { container_diagnostics.push(diagnostics.clone()); } else { - debug_assert!( - false, + panic!( "diagnostics vector out of sync with containers (idx = {}, len = {})", idx, container_diagnostics.len() From 03e903e26be58d8ac83cc0beeb79113c918fc9e0 Mon Sep 17 00:00:00 2001 From: Jonas Pfalzgraf Date: Wed, 29 Oct 2025 20:50:09 +0100 Subject: [PATCH 07/19] Update web/script.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- web/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/script.js b/web/script.js index a8a38d1..ecd8441 100644 --- a/web/script.js +++ b/web/script.js @@ -220,7 +220,7 @@ function updateStats(container, dims, visibleCount = null) { const summaryHtml = summary ? `
-

Diagnose (gesamt):

+

Diagnostik (gesamt):

Max. Ungleichgewicht: ${formatPercent(summary.max_imbalance_ratio)}

Unterstützung Ø / min: ${formatPlainPercent( summary.average_support_percent From 2c826f2f32da4fbd2b5d91589c3ba18efa187dbc Mon Sep 17 00:00:00 2001 From: Jonas Pfalzgraf Date: Wed, 29 Oct 2025 20:50:29 +0100 Subject: [PATCH 08/19] Update src/optimizer.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/optimizer.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/optimizer.rs b/src/optimizer.rs index f0c07a3..2ea32ec 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -117,6 +117,14 @@ impl PackingConfigBuilder { } /// Abstrakte Strategien zur Gruppierung/Neuordnung von Objekten vor dem Packen. +/// +/// Diese interne Trait definiert die Schnittstelle für Strategien, die die Reihenfolge +/// (und ggf. Auswahl) von Objekten vor dem Packvorgang beeinflussen. Implementierungen +/// können die Reihenfolge der Objekte ändern, Gruppen bilden oder Objekte filtern, um +/// die Effizienz des Packens zu verbessern. Es wird garantiert, dass die Rückgabe +/// eine (ggf. gefilterte) Teilmenge der Eingabe ist; Objekte können entfernt, aber +/// nicht modifiziert werden. Die Trait ist absichtlich privat, da sie nur für interne +/// Optimierungsstrategien gedacht ist und keine stabile API garantiert. trait ObjectClusterStrategy { fn reorder(&self, objects: Vec) -> Vec; } From 9d9ba12edfd0f7a44b24fdcb012fe232c2fbc2be Mon Sep 17 00:00:00 2001 From: Jonas Pfalzgraf Date: Wed, 29 Oct 2025 20:50:58 +0100 Subject: [PATCH 09/19] Update src/config.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/config.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config.rs b/src/config.rs index fe50bc3..f5da760 100644 --- a/src/config.rs +++ b/src/config.rs @@ -212,6 +212,7 @@ impl OptimizerConfig { let footprint_cluster_tolerance = load_f64_with_warning( Self::FOOTPRINT_TOLERANCE_VAR, PackingConfig::DEFAULT_FOOTPRINT_CLUSTER_TOLERANCE, + // Values above 0.5 would group excessively dissimilar footprints, defeating the clustering purpose. |value| (0.0..=0.5).contains(&value), "muss zwischen 0 und 0.5 liegen", "Warnung: Angepasste Footprint-Gruppierung kann zu unerwarteten Platzierungen führen", From aaa2251be5fcff78ccc071b3767893cba2f2ca41 Mon Sep 17 00:00:00 2001 From: JonasPfalzgraf <20913954+JosunLP@users.noreply.github.com> Date: Wed, 29 Oct 2025 21:35:50 +0100 Subject: [PATCH 10/19] =?UTF-8?q?=F0=9F=90=9B=20Verbessere=20die=20Berechn?= =?UTF-8?q?ung=20des=20Unterst=C3=BCtzungsbereichs=20in=20der=20Optimierun?= =?UTF-8?q?gslogik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api.rs | 1 + src/optimizer.rs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/api.rs b/src/api.rs index 945c9b8..af8593c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -37,6 +37,7 @@ struct ApiState { static OPENAPI_DOC: OnceLock = OnceLock::new(); +// SRI hashes verified against https://unpkg.com/swagger-ui-dist@5.17.14/ on 2025-10-29. const SWAGGER_UI_HTML: &str = r##" diff --git a/src/optimizer.rs b/src/optimizer.rs index f0c07a3..8f4c060 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -864,7 +864,8 @@ fn support_ratio_of(b: &PlacedBox, cont: &Container, config: &PackingConfig) -> let (bx, by, bz) = b.position; let (bw, bd, _) = b.object.dims; let base_area = bw * bd; - if base_area <= config.general_epsilon { + let min_support_area = config.general_epsilon * config.general_epsilon; + if base_area <= min_support_area { return 0.0; } @@ -892,7 +893,8 @@ fn has_sufficient_support(b: &PlacedBox, cont: &Container, config: &PackingConfi return true; } - support_ratio_of(b, cont, config) + config.general_epsilon >= config.support_ratio + let required_support = (config.support_ratio - config.general_epsilon).max(0.0); + support_ratio_of(b, cont, config) >= required_support } /// Prüft, ob der Schwerpunkt des Objekts (Projektion auf XY) von der Auflagefläche getragen wird. From 64d7dfad3fb803dcebfcd50278ffe8a93164cea1 Mon Sep 17 00:00:00 2001 From: JonasPfalzgraf <20913954+JosunLP@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:02:40 +0100 Subject: [PATCH 11/19] =?UTF-8?q?=F0=9F=9A=91=20Optimieren=20der=20CodeQL-?= =?UTF-8?q?Workflow-Konfiguration:=20Vereinheitlichung=20der=20Formatierun?= =?UTF-8?q?g=20und=20Anpassung=20des=20Build-Modus=20f=C3=BCr=20Rust?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/codeql.yml | 89 +++++++++++++++++------------------- 1 file changed, 42 insertions(+), 47 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fedf0a3..e363801 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,11 +13,11 @@ name: "CodeQL Advanced" on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] schedule: - - cron: '37 0 * * 6' + - cron: "37 0 * * 6" jobs: analyze: @@ -43,12 +43,10 @@ jobs: fail-fast: false matrix: include: - - language: actions - build-mode: none - - language: javascript-typescript - build-mode: none - - language: rust - build-mode: none + - language: javascript-typescript + build-mode: none + - language: rust + build-mode: manual # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -58,46 +56,43 @@ jobs: # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - # Add any setup steps before running the `github/codeql-action/init` action. - # This includes steps like installing compilers or runtimes (`actions/setup-node` - # or others). This is typically only required for manual builds. - # - name: Setup runtime (example) - # uses: actions/setup-example@v1 + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - name: Run manual build steps - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 + - name: Install Rust toolchain + if: matrix.language == 'rust' + uses: dtolnay/rust-toolchain@stable - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{matrix.language}}" + # Use a manual build so the generated SARIF accurately represents the Rust binaries we ship. + - name: Build crate + if: matrix.build-mode == 'manual' + shell: bash + run: | + set -euo pipefail + if [[ "${{ matrix.language }}" == "rust" ]]; then + cargo build --locked --all-targets + fi + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" From d691cc3ca8d783edb232c78e4c6f084a9ddc59d3 Mon Sep 17 00:00:00 2001 From: Jonas Pfalzgraf Date: Wed, 29 Oct 2025 22:07:42 +0100 Subject: [PATCH 12/19] Update issue templates (#5) --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 9e3e5709c9213255b839f3adf4c23aad709b0ec3 Mon Sep 17 00:00:00 2001 From: Jonas Pfalzgraf Date: Wed, 29 Oct 2025 22:08:10 +0100 Subject: [PATCH 13/19] Update funding information in FUNDING.yml (#6) * Update funding information in FUNDING.yml * Update .github/FUNDING.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/FUNDING.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..dc196c8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: [josunlp] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: josunlp +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From a839dc77b6a84fe67028cbd44f25207376f0cdb7 Mon Sep 17 00:00:00 2001 From: JonasPfalzgraf <20913954+JosunLP@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:11:10 +0100 Subject: [PATCH 14/19] =?UTF-8?q?=F0=9F=93=9D=20Erstelle=20CHANGELOG=20f?= =?UTF-8?q?=C3=BCr=20die=20erste=20Version=201.0.0=20von=20sort=5Fit=5Fnow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CHANGELOG diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..d6a3c1a --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,5 @@ +# Changelog + +## [1.0.0] - 29-10-2025 + +- Initial release of sort_it_now. From 097c964b81e87090f5c0ca167c31c40b1d4c09b6 Mon Sep 17 00:00:00 2001 From: JonasPfalzgraf <20913954+JosunLP@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:14:40 +0100 Subject: [PATCH 15/19] =?UTF-8?q?=E2=9C=A8=20F=C3=BCge=20Labeler-Konfigura?= =?UTF-8?q?tion=20f=C3=BCr=20Pull=20Requests=20hinzu:=20Automatisches=20Hi?= =?UTF-8?q?nzuf=C3=BCgen=20von=20Labels=20basierend=20auf=20ge=C3=A4nderte?= =?UTF-8?q?n=20Dateipfaden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/labeler.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..6ccebee --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,14 @@ +# Add labels to pull requests based on modified file paths +rust: + - 'src/**/*.rs' + - 'Cargo.toml' +web: + - 'web/**' +scripts: + - 'scripts/**' +config: + - '.github/workflows/**' + - 'package.json' +documentation: + - 'README.md' + - 'CONCEPT.md' From b364435b5fc0feb77523856306384a861269628e Mon Sep 17 00:00:00 2001 From: Jonas Pfalzgraf Date: Wed, 29 Oct 2025 22:15:11 +0100 Subject: [PATCH 16/19] Update .github/workflows/codeql.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e363801..932c1ec 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -89,7 +89,7 @@ jobs: run: | set -euo pipefail if [[ "${{ matrix.language }}" == "rust" ]]; then - cargo build --locked --all-targets + cargo build --locked --release fi - name: Perform CodeQL Analysis From f9751f536a4a98f1ac9bb92fa54d4c61ef43e1c9 Mon Sep 17 00:00:00 2001 From: JonasPfalzgraf <20913954+JosunLP@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:17:06 +0100 Subject: [PATCH 17/19] =?UTF-8?q?=F0=9F=90=9B=20=C3=84ndere=20Build-Modus?= =?UTF-8?q?=20f=C3=BCr=20Rust=20von=20manuell=20auf=20automatisch,=20um=20?= =?UTF-8?q?die=20CodeQL-Analyse=20zu=20optimieren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 932c1ec..eda5532 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: - language: javascript-typescript build-mode: none - language: rust - build-mode: manual + build-mode: none # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -82,7 +82,7 @@ jobs: if: matrix.language == 'rust' uses: dtolnay/rust-toolchain@stable - # Use a manual build so the generated SARIF accurately represents the Rust binaries we ship. + # Run an explicit build only when using manual mode for compiled languages. - name: Build crate if: matrix.build-mode == 'manual' shell: bash From e76fb95384695b36568f50970d84b9e9895dc5d9 Mon Sep 17 00:00:00 2001 From: JonasPfalzgraf <20913954+JosunLP@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:20:22 +0100 Subject: [PATCH 18/19] =?UTF-8?q?=F0=9F=90=9B=20=C3=84ndere=20Build-Modus?= =?UTF-8?q?=20f=C3=BCr=20Rust=20von=20'none'=20auf=20'autobuild'=20zur=20O?= =?UTF-8?q?ptimierung=20der=20CodeQL-Analyse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/codeql.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index eda5532..1816641 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: - language: javascript-typescript build-mode: none - language: rust - build-mode: none + build-mode: autobuild # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -82,6 +82,10 @@ jobs: if: matrix.language == 'rust' uses: dtolnay/rust-toolchain@stable + - name: Autobuild + if: matrix.build-mode == 'autobuild' + uses: github/codeql-action/autobuild@v4 + # Run an explicit build only when using manual mode for compiled languages. - name: Build crate if: matrix.build-mode == 'manual' From 0329ff2f4d53d7027444ab6d829e5bf1d8723758 Mon Sep 17 00:00:00 2001 From: JonasPfalzgraf <20913954+JosunLP@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:25:50 +0100 Subject: [PATCH 19/19] =?UTF-8?q?=F0=9F=90=9B=20=C3=84ndere=20Build-Modus?= =?UTF-8?q?=20f=C3=BCr=20Rust=20von=20'autobuild'=20auf=20'none'=20zur=20A?= =?UTF-8?q?npassung=20der=20CodeQL-Analyse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/codeql.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1816641..6474dce 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: - language: javascript-typescript build-mode: none - language: rust - build-mode: autobuild + build-mode: none # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -82,10 +82,6 @@ jobs: if: matrix.language == 'rust' uses: dtolnay/rust-toolchain@stable - - name: Autobuild - if: matrix.build-mode == 'autobuild' - uses: github/codeql-action/autobuild@v4 - # Run an explicit build only when using manual mode for compiled languages. - name: Build crate if: matrix.build-mode == 'manual' @@ -100,3 +96,4 @@ jobs: uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" + upload: false