cel_euclid/lib.rs
1//! Opt-in `f64` / [`euclid::UnknownUnit`] geometry for CEL.
2//!
3//! Install once on a fresh lookup; no parser or runtime built-ins change.
4//! Named operations support ordinary calls and receiver calls (including
5//! zero-argument property syntax). Operators require exact operand types.
6//!
7//! ```
8//! use cel_parser::{CELParser, OpLookup};
9//! let mut lookup = OpLookup::new();
10//! cel_euclid::install(&mut lookup);
11//! let mut parser = CELParser::new(lookup);
12//! let mut segment = parser.parse_str(
13//! "(Point2D { y: 2.0, x: 1.0 } + Vector2D { x: 3.0, y: 4.0 }).x"
14//! ).unwrap();
15//! assert_eq!(segment.call0::<f64>().unwrap(), 4.0);
16//! ```
17//!
18//! Geometry arithmetic, bounds and containment retain Euclid semantics, including
19//! non-finite arithmetic and half-open containment. Three operations are checked:
20//! `normalize` rejects zero/non-finite lengths and non-finite results; `inverse`
21//! rejects non-finite coefficients, zero/non-finite determinants, non-finite
22//! inverses and zero inverse coefficients with nonzero exact cofactors;
23//! `outer_transformed_rect` rejects invalid or unrepresentable bounds.
24//! Ordinary point/vector mapping, constructors and composition remain unchecked.
25//! Checked inversion permits exact-zero cofactors and nonzero subnormal results.
26//! Translation cofactor checks compare exact products, so underflow or rounding
27//! before determinant scaling cannot disguise a lost nonzero coefficient.
28//! Other rounding and conditioning errors retain Euclid semantics: checked
29//! inversion does not guarantee exact-real arithmetic or numerical stability.
30//!
31//! `degrees` and `radians` construct angles from `f64` and extract the respective
32//! scalar from angles. Matrix accessors use Euclid's six coefficient names.
33//! `a.then(b)` applies `a` first, then `b`; vectors ignore translation.
34//!
35//! ```
36//! use cel_parser::{CELParser, OpLookup};
37//! let mut lookup = OpLookup::new();
38//! cel_euclid::install(&mut lookup);
39//! let mut parser = CELParser::new(lookup);
40//! let mut segment = parser.parse_str(
41//! "translation(Vector2D { x: 10.0, y: 0.0 }).then(scale(2.0, 2.0))\
42//! .transform_point(Point2D { x: 1.0, y: 0.0 }).x"
43//! ).unwrap();
44//! assert_eq!(segment.call0::<f64>().unwrap(), 22.0);
45//! ```
46//!
47//! Checked outer bounds enclose all four Euclid-mapped corners, not just a
48//! diagonal, for arbitrary affine rotation, shear and reflection. They are not
49//! an interior-containment test. Inputs must have finite matrix coefficients,
50//! finite source origins/extents/endpoints and nonnegative source extents.
51//! Positive source extents must advance their endpoints. Projected edges must
52//! not overflow or completely underflow, and positive projected spans must not
53//! collapse after mapping. Mapped corners and resulting bounds must be finite.
54//! Zero-size rectangles and singular transforms are allowed. If reconstructing
55//! a bound's maximum rounds inward, its extent advances one ULP to enclose that
56//! maximum; unrepresentable extents/endpoints return evaluation errors.
57//! The `adam` feature only adds optional type registration; it is disabled by default.
58//!
59//! # Optional Adam installation
60//!
61//! Hosts explicitly install both cell types and operations. `adam-lang` itself
62//! has no geometry dependency. Its registration-aware checker consumes the same
63//! registry and lookup as compilation; unresolved call results remain best-effort.
64//! Bare CEL hosts that use registered closure annotations must also configure
65//! their [`cel_parser::TypeResolver`]; installing record operations alone does
66//! not replace the parser's annotation resolver.
67//!
68//! ```
69//! # #[cfg(feature = "adam")] {
70//! let mut types = adam_lang::TypeRegistry::new();
71//! cel_euclid::register_adam_types(&mut types);
72//! let mut lookup = cel_parser::OpLookup::new();
73//! cel_euclid::install(&mut lookup);
74//! let mut parser = adam_lang::AdamParser::new(types, lookup);
75//! assert!(parser.parse_str("sheet s { cell p: Point2D; }").is_ok());
76//! # }
77//! ```
78//!
79//! Run `cargo run -p cel-euclid --features adam --example adam_geometry` for
80//! source-preserving crop/straighten and pan/zoom sheets. Crop fitting, its
81//! eight-epsilon inward guard, and precision/coverage requirements live in CEL,
82//! not in library operations. The example's Rust boundary validates inputs only.
83//!
84//! # Composing numeric image constraints
85//!
86//! With `adam`, the shipped component-only `image_constraints` module composes
87//! two resampling axes, a caller-selected aspect relation and crop fitting through
88//! caller-owned ordinary cells.
89//! Register the importing root too, and install both optional operation libraries
90//! before creating the immutable compiler:
91//!
92//! ```
93//! # #[cfg(feature = "adam")] {
94//! let mut types = adam_lang::TypeRegistry::new();
95//! cel_euclid::register_adam_types(&mut types);
96//! let mut lookup = cel_parser::OpLookup::new();
97//! cel_std::install(&mut lookup);
98//! cel_euclid::install(&mut lookup);
99//! let mut modules = adam_lang::ModuleSet::new();
100//! modules.insert_source("image_constraints".parse().unwrap(), "image_constraints.adm2",
101//! include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/image_constraints.adm2"))).unwrap();
102//! modules.insert_source("resize_straighten".parse().unwrap(), "resize_straighten.adm2",
103//! include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/resize_straighten.adm2"))).unwrap();
104//! let compiled = adam_lang::ModuleCompiler::new(types, lookup).compile(modules).unwrap();
105//! let mut parsed = compiled.instantiate_root(
106//! &"resize_straighten".parse().unwrap(), "resize_straighten").unwrap();
107//! drop(compiled);
108//! let crop = parsed.cell_names["crop"].0;
109//! let requested_crop = *parsed.source::<cel_euclid::Rect>(crop).unwrap();
110//! let width = parsed.cell_names["width_inches"].0;
111//! let height = parsed.cell_names["height_inches"].0;
112//! parsed.sheet.write(width, 4.0f64).unwrap();
113//! parsed.propagate().unwrap();
114//! assert_eq!(*parsed.read::<f64>(height).unwrap(), 3.0);
115//! assert_eq!(*parsed.source::<f64>(width).unwrap(), 4.0);
116//! assert!(parsed.cell_requirements_valid(crop));
117//! assert!(*parsed.read::<bool>(parsed.cell_names["coverage"].0).unwrap());
118//! assert!(*parsed.read::<bool>(parsed.cell_names["precise"].0).unwrap());
119//! assert_eq!(*parsed.source::<cel_euclid::Rect>(crop).unwrap(), requested_crop);
120//! parsed.sheet.write(height, 4.0f64).unwrap();
121//! parsed.propagate().unwrap();
122//! assert_eq!(*parsed.read::<f64>(width).unwrap(), 1600.0 / 300.0);
123//! assert_eq!(*parsed.source::<f64>(height).unwrap(), 4.0);
124//! assert_eq!(*parsed.read::<cel_euclid::Rect>(crop).unwrap(), requested_crop);
125//! # }
126//! ```
127//!
128//! The caller owns pixel filters, source resolution/aspect/transform, positive
129//! finite dimension/ratio requirements, image/crop requirements and precision
130//! requirements. The aspect relation uses the two floating multiplication
131//! directions for a positive finite, nonzero ratio; selected products/quotients
132//! must remain representable. It expresses equal normalized width/height
133//! percentages without importing the original UI's percentage/toggle machinery.
134//! Each axis retains independent ties-even pixel conversion and checked casts;
135//! the physical aspect relation does not promise an exact integer pixel ratio.
136//! The crop fit retains the existing 8/32/64-epsilon guards. Its requested image
137//! anchor must lie strictly inside the image. Invalid inverses/conversions return
138//! evaluation errors; inadequate finite precision invalidates an explicit
139//! requirement. A successful propagation alone does not certify a valid result.
140//! Neither example changes the general Euclid operations or permits `out` actuals.
141
142#![doc = include_str!("../SIGNATURES.md")]
143
144#[cfg(feature = "adam")]
145mod adam;
146mod geometry;
147mod records;
148mod transforms;
149
150#[cfg(feature = "adam")]
151pub use adam::register_adam_types;
152
153/// A two-dimensional point with `f64` coordinates and unknown units.
154///
155/// # Examples
156/// ```
157/// assert_eq!(cel_euclid::Point2D::default(), cel_euclid::Point2D::new(0.0, 0.0));
158/// ```
159pub type Point2D = euclid::default::Point2D<f64>;
160/// A two-dimensional vector with `f64` components and unknown units.
161///
162/// # Examples
163/// ```
164/// assert_eq!(cel_euclid::Vector2D::default(), cel_euclid::Vector2D::zero());
165/// ```
166pub type Vector2D = euclid::default::Vector2D<f64>;
167/// A two-dimensional size with `f64` extents and unknown units.
168///
169/// # Examples
170/// ```
171/// assert_eq!(cel_euclid::Size2D::default(), cel_euclid::Size2D::zero());
172/// ```
173pub type Size2D = euclid::default::Size2D<f64>;
174/// A rectangle represented by an origin and size in unknown units.
175///
176/// # Examples
177/// ```
178/// assert_eq!(cel_euclid::Rect::default().size, cel_euclid::Size2D::zero());
179/// ```
180pub type Rect = euclid::default::Rect<f64>;
181/// An axis-aligned box represented by minimum and maximum points in unknown units.
182///
183/// # Examples
184/// ```
185/// assert_eq!(cel_euclid::Box2D::default().max, cel_euclid::Point2D::zero());
186/// ```
187pub type Box2D = euclid::default::Box2D<f64>;
188/// An angle stored as `f64` radians.
189///
190/// # Examples
191/// ```
192/// assert_eq!(cel_euclid::Angle::default().radians, 0.0);
193/// ```
194pub type Angle = euclid::Angle<f64>;
195/// A two-dimensional affine matrix mapping unknown units to unknown units.
196///
197/// # Examples
198/// ```
199/// assert_eq!(cel_euclid::Transform2D::default(), cel_euclid::Transform2D::identity());
200/// ```
201pub type Transform2D = euclid::default::Transform2D<f64>;
202
203/// Installs seven record schemas and the complete exact geometry signature table.
204///
205/// - Precondition: this library has not already been installed on `lookup`.
206/// - Precondition: `lookup` has no record schemas named `Point2D`, `Vector2D`,
207/// `Size2D`, `Rect`, `Box2D`, `Angle`, or `Transform2D`.
208/// - Precondition: `lookup` has no operation with the same name and operand
209/// identities as a signature in the [supported table](#supported-geometry-signatures).
210/// Other overloads of those names may already be installed.
211/// - Postcondition: unrelated signatures, including scalar `min`/`max`, remain available.
212/// - Complexity: O(s²) time and O(s) space in the total installed signatures.
213///
214/// # Examples
215/// ```
216/// let mut lookup = cel_parser::OpLookup::new();
217/// cel_euclid::install(&mut lookup);
218/// assert!(lookup.record_schema("Rect").is_some());
219/// ```
220pub fn install(lookup: &mut cel_parser::OpLookup) {
221 debug_assert!(
222 [
223 "Point2D",
224 "Vector2D",
225 "Size2D",
226 "Rect",
227 "Box2D",
228 "Angle",
229 "Transform2D"
230 ]
231 .iter()
232 .all(|name| lookup.record_schema(name).is_none())
233 );
234 records::install(lookup)
235 .expect("cel-euclid record names must not conflict with existing schemas");
236 geometry::install(lookup)
237 .expect("cel-euclid operation signatures must not conflict with existing signatures");
238 transforms::install(lookup)
239 .expect("cel-euclid transform signatures must not conflict with existing signatures");
240}
241
242/// Describes a concrete leaf without erasing its Rust identity.
243fn leaf<T: 'static>(name: &'static str) -> cel_parser::ResolvedLeafType {
244 cel_parser::ResolvedLeafType::new(name, cel_runtime::ValueType::leaf::<T>())
245}