jagua_rs/io/ext_repr.rs
1use crate::geometry::DTransformation;
2use serde::{Deserialize, Serialize};
3
4/// External representation of an [`Item`](crate::entities::Item).
5#[derive(Serialize, Clone)]
6pub struct ExtItem {
7 /// Unique external identifier of the item.
8 pub id: u64,
9 /// Required orientation permissions in original item coordinates.
10 pub orientation: ExtOrientation,
11 /// Shape of the item. Polygons with holes and multipolygons are not supported.
12 pub shape: ExtShape,
13 /// Minimum required quality in `0..N_QUALITIES`, validated at import.
14 /// Missing or null requires full quality. Holes always block placement.
15 pub min_quality: Option<usize>,
16}
17
18/// Orientation permissions in degrees. Both this object and its rotation field are required.
19#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
20#[serde(deny_unknown_fields)]
21pub struct ExtOrientation {
22 /// Permitted rotations, applied after optional reflection.
23 pub rotation: ExtRotation,
24 /// Optional reflection axes through the original item's local origin,
25 /// in degrees counterclockwise from its positive x-axis, before rotation.
26 /// 0° is the horizontal x-axis: `(x, y)` becomes `(x, -y)`.
27 /// 90° is the vertical y-axis: `(x, y)` becomes `(-x, y)`.
28 /// Missing or empty disables reflection.
29 /// Choose no reflection or one axis; equivalent axes are normalized modulo 180°.
30 #[serde(default, skip_serializing_if = "Vec::is_empty")]
31 pub reflection_axes: Vec<f32>,
32}
33
34/// Explicit rotation modes, validated at import.
35/// Lists and stepped expansions are limited to 65,536 angles.
36#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
37#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
38pub enum ExtRotation {
39 /// A nonempty list of finite angles in degrees. Zero is not implicit.
40 /// Equivalent angles are normalized modulo 360 and deduplicated.
41 Discrete { angles: Vec<f32> },
42 /// Zero-based increments covering one turn, excluding the duplicate endpoint.
43 /// The finite step must be in (0, 360]. In f64 arithmetic, round `360 / step`
44 /// to a count and require `abs(count * step - 360) <= 360 * f32::EPSILON`.
45 /// Expand by index as `i * 360 / count`, avoiding accumulated rounding error.
46 Stepped { step: f32 },
47 /// Any rotation angle. No null or empty-list sentinel is used.
48 Continuous {},
49}
50
51impl<'de> Deserialize<'de> for ExtItem {
52 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
53 // Reject obsolete permissions even in flattened demand items, while allowing
54 // unrelated metadata such as source filenames.
55 #[derive(Deserialize)]
56 struct Input {
57 id: u64,
58 orientation: ExtOrientation,
59 shape: ExtShape,
60 min_quality: Option<usize>,
61 #[serde(
62 default,
63 rename = "allowed_orientations",
64 alias = "allowed_rotations",
65 alias = "allowed_reflection_axes",
66 deserialize_with = "reject_legacy"
67 )]
68 legacy: (),
69 }
70 fn reject_legacy<'de, D: serde::Deserializer<'de>>(_: D) -> Result<(), D::Error> {
71 Err(serde::de::Error::custom(
72 "legacy orientation fields are not supported; use orientation.rotation",
73 ))
74 }
75 let Input {
76 id,
77 orientation,
78 shape,
79 min_quality,
80 legacy: (),
81 } = Input::deserialize(deserializer)?;
82 Ok(Self {
83 id,
84 orientation,
85 shape,
86 min_quality,
87 })
88 }
89}
90
91/// External representation of a [`Container`](crate::entities::Container).
92/// Items can be placed inside containers.
93#[derive(Serialize, Deserialize, Clone)]
94pub struct ExtContainer {
95 /// Unique identifier of the container
96 pub id: u64,
97 /// Shape of the container
98 pub shape: ExtShape,
99 /// Zones within the container with varying quality. Holes in the container shape are treated as zones with quality 0.
100 #[serde(skip_serializing_if = "Vec::is_empty", default)]
101 pub zones: Vec<ExtQualityZone>,
102}
103
104/// Various ways to represent a shape
105#[derive(Serialize, Deserialize, Clone)]
106#[serde(tag = "type", content = "data")]
107#[serde(rename_all = "snake_case")]
108pub enum ExtShape {
109 /// Axis-aligned rectangle. With its left bottom corner at (`x_min`, `y_min`), a width and height
110 Rectangle {
111 x_min: f32,
112 y_min: f32,
113 width: f32,
114 height: f32,
115 },
116 /// Polygon with a single outer boundary
117 SimplePolygon(ExtSPolygon),
118 /// Polygon with a single outer boundary and a set of holes
119 Polygon(ExtPolygon),
120 /// Multiple disjoint polygons
121 MultiPolygon(Vec<ExtPolygon>),
122}
123
124/// A polygon represented as an outer boundary and a list of holes
125#[derive(Serialize, Deserialize, Clone)]
126pub struct ExtPolygon {
127 /// The outer boundary of the polygon
128 pub outer: ExtSPolygon,
129 /// A list of holes in the polygon
130 #[serde(default)]
131 pub inner: Vec<ExtSPolygon>,
132}
133
134/// External representation of a [`SPolygon`](crate::geometry::primitives::SPolygon).
135/// A polygon with no holes and no self-intersections.
136#[derive(Serialize, Deserialize, Clone)]
137pub struct ExtSPolygon(pub Vec<(f32, f32)>);
138
139/// A zone with a specific quality level
140#[derive(Serialize, Deserialize, Clone)]
141pub struct ExtQualityZone {
142 /// The quality level of this zone
143 pub quality: usize,
144 /// The polygon shape of this zone
145 pub shape: ExtShape,
146}
147
148/// External representation of a [`Layout`](crate::entities::Layout).
149/// A layout consists of a container with items placed in a specific configuration.
150#[derive(Serialize, Deserialize, Clone)]
151pub struct ExtLayout {
152 /// The container that was used
153 pub container_id: u64,
154 /// The items placed in the container and where they were placed
155 pub placed_items: Vec<ExtPlacedItem>,
156 /// Some statistics about the layout
157 pub density: f32,
158}
159
160/// External representation of a [`PlacedItem`](crate::entities::PlacedItem).
161/// An item placed in a container with a specific transformation.
162#[derive(Serialize, Deserialize, Clone)]
163pub struct ExtPlacedItem {
164 /// The id of the item in the instance
165 pub item_id: u64,
166 /// The transformation applied to the item to place it in the container
167 pub transformation: ExtTransformation,
168}
169
170/// Optional x-axis reflection, followed by rotation and translation.
171/// The canonical x-axis reflection is independent of the item's permitted input axes:
172/// reflecting across axis `a` then rotating by `r` is exported with rotation `r + 2*a`.
173#[derive(Serialize, Deserialize, Clone)]
174pub struct ExtTransformation {
175 /// Negate the original local y coordinate before rotation. Defaults to false.
176 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
177 pub reflected: bool,
178 /// The rotation angle in degrees
179 pub rotation: f32,
180 /// The translation vector (x, y)
181 pub translation: (f32, f32),
182}
183
184impl From<DTransformation> for ExtTransformation {
185 fn from(dt: DTransformation) -> Self {
186 ExtTransformation {
187 reflected: dt.reflected,
188 rotation: dt.rotation().to_degrees(),
189 translation: dt.translation(),
190 }
191 }
192}
193
194impl From<ExtTransformation> for DTransformation {
195 fn from(ext_dt: ExtTransformation) -> Self {
196 DTransformation::new(ext_dt.rotation.to_radians(), ext_dt.translation)
197 .with_reflection(ext_dt.reflected)
198 }
199}