Skip to main content

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#![doc = include_str!("../SIGNATURES.md")]
85
86#[cfg(feature = "adam")]
87mod adam;
88mod geometry;
89mod records;
90mod transforms;
91
92#[cfg(feature = "adam")]
93pub use adam::register_adam_types;
94
95/// A two-dimensional point with `f64` coordinates and unknown units.
96///
97/// # Examples
98/// ```
99/// assert_eq!(cel_euclid::Point2D::default(), cel_euclid::Point2D::new(0.0, 0.0));
100/// ```
101pub type Point2D = euclid::default::Point2D<f64>;
102/// A two-dimensional vector with `f64` components and unknown units.
103///
104/// # Examples
105/// ```
106/// assert_eq!(cel_euclid::Vector2D::default(), cel_euclid::Vector2D::zero());
107/// ```
108pub type Vector2D = euclid::default::Vector2D<f64>;
109/// A two-dimensional size with `f64` extents and unknown units.
110///
111/// # Examples
112/// ```
113/// assert_eq!(cel_euclid::Size2D::default(), cel_euclid::Size2D::zero());
114/// ```
115pub type Size2D = euclid::default::Size2D<f64>;
116/// A rectangle represented by an origin and size in unknown units.
117///
118/// # Examples
119/// ```
120/// assert_eq!(cel_euclid::Rect::default().size, cel_euclid::Size2D::zero());
121/// ```
122pub type Rect = euclid::default::Rect<f64>;
123/// An axis-aligned box represented by minimum and maximum points in unknown units.
124///
125/// # Examples
126/// ```
127/// assert_eq!(cel_euclid::Box2D::default().max, cel_euclid::Point2D::zero());
128/// ```
129pub type Box2D = euclid::default::Box2D<f64>;
130/// An angle stored as `f64` radians.
131///
132/// # Examples
133/// ```
134/// assert_eq!(cel_euclid::Angle::default().radians, 0.0);
135/// ```
136pub type Angle = euclid::Angle<f64>;
137/// A two-dimensional affine matrix mapping unknown units to unknown units.
138///
139/// # Examples
140/// ```
141/// assert_eq!(cel_euclid::Transform2D::default(), cel_euclid::Transform2D::identity());
142/// ```
143pub type Transform2D = euclid::default::Transform2D<f64>;
144
145/// Installs seven record schemas and the complete exact geometry signature table.
146///
147/// - Precondition: this library has not already been installed on `lookup`.
148/// - Precondition: `lookup` has no record schemas named `Point2D`, `Vector2D`,
149///   `Size2D`, `Rect`, `Box2D`, `Angle`, or `Transform2D`.
150/// - Precondition: `lookup` has no operation with the same name and operand
151///   identities as a signature in the [supported table](#supported-geometry-signatures).
152///   Other overloads of those names may already be installed.
153/// - Postcondition: unrelated signatures, including scalar `min`/`max`, remain available.
154/// - Complexity: O(s²) time and O(s) space in the total installed signatures.
155///
156/// # Examples
157/// ```
158/// let mut lookup = cel_parser::OpLookup::new();
159/// cel_euclid::install(&mut lookup);
160/// assert!(lookup.record_schema("Rect").is_some());
161/// ```
162pub fn install(lookup: &mut cel_parser::OpLookup) {
163    debug_assert!(
164        [
165            "Point2D",
166            "Vector2D",
167            "Size2D",
168            "Rect",
169            "Box2D",
170            "Angle",
171            "Transform2D"
172        ]
173        .iter()
174        .all(|name| lookup.record_schema(name).is_none())
175    );
176    records::install(lookup)
177        .expect("cel-euclid record names must not conflict with existing schemas");
178    geometry::install(lookup)
179        .expect("cel-euclid operation signatures must not conflict with existing signatures");
180    transforms::install(lookup)
181        .expect("cel-euclid transform signatures must not conflict with existing signatures");
182}
183
184/// Describes a concrete leaf without erasing its Rust identity.
185fn leaf<T: 'static>(name: &'static str) -> cel_parser::ResolvedLeafType {
186    cel_parser::ResolvedLeafType::new(name, cel_runtime::ValueType::leaf::<T>())
187}