axiolid_model/station.rs
1//! Stations: positions named by a distance along a basis curve (#241,
2//! ADR 0082).
3//!
4//! A station is exact stored data -- a distance and three offsets -- that a
5//! kernel resolves by evaluating the basis curve; the graph never does.
6//!
7//! # Distance
8//!
9//! [`Station::distance`] is measured from the basis curve's start in that
10//! curve's own convention: PLAN distance on an elevated or a banked curve
11//! (their parameter), arc length on every other curve, 2D or 3D. The start
12//! is parameter `0`, or the first parameter of the domain of a polyline or
13//! a B-spline. A resolver refuses by name a distance before the start or
14//! past the curve's length; the graph refuses a non-finite or negative one
15//! when the node is pushed.
16//!
17//! A basis may also be a curve relation a distance runs along (#285): a
18//! [`CurveRelation::Composite`], a [`CurveRelation::Trimmed`], a curve
19//! placed by an [`InstanceAtStation`], nested in any combination. The
20//! distance then runs end to end through the atomic curves it is made of,
21//! each span in its own curve's convention, and those must agree: plan
22//! distance when every span is an elevated or banked curve, arc length
23//! when none is. A composite of segments placed at stations of another
24//! curve is measured along its own segments, whatever curve they were
25//! placed on. Each joint of a composite is a seam (below), and its pieces
26//! must meet end to start; a resolver refuses by name pieces measured
27//! differently, a gap, and a piece whose undeclared sense runs it
28//! backwards.
29//!
30//! [`CurveRelation::Composite`]: crate::CurveRelation::Composite
31//! [`CurveRelation::Trimmed`]: crate::CurveRelation::Trimmed
32//!
33//! # Frame and offsets
34//!
35//! The basis curve's section frame at the distance has three unit axes:
36//! the tangent, the lateral axis to the LEFT of it, and `up = tangent x
37//! lateral`. For a 2D curve (lying in `z = 0`) they are the tangent, the
38//! left normal and `+Z`; for a banked curve, its rolled section (lateral
39//! towards the left rail head); for every other 3D curve, an elevated one
40//! included, the reference-up frame against `+Z` (lateral horizontal, up
41//! leaning back with the grade). [`StationFrame::Plan`] instead takes the
42//! tangent's horizontal projection, the horizontal left normal and `+Z`.
43//!
44//! The station's point is `point + lateral * l + up * v + tangent * g` for
45//! the offsets `(l, v, g)` of [`StationOffsets`]. A resolver presents the
46//! frame as `x` the tangent, `y` up, `z` to the right (`-lateral`), the
47//! curve-evaluation provider's layout.
48//!
49//! # Explicit orientation (#246)
50//!
51//! A station may carry a [`StationOrientation`]: an optional `axis` and an
52//! optional `ref_direction`, both given as COMPONENTS IN THE STATION'S BASE
53//! FRAME at its distance (the [`StationFrame`] it names), never in world
54//! coordinates: a vector `(a, b, c)` means `a * tangent + b * lateral + c *
55//! up`. This is how a linear placement reads its own axes: they "are
56//! relative to the curve used for linear referencing ..., maintaining the
57//! relationship to the tangent of the curve" (buildingSMART IFC 4.3,
58//! `IfcAxis2PlacementLinear`), whose local `X` is the tangent, `Y` the left
59//! lateral and `Z` up, this module's `(tangent, lateral, up)`.
60//!
61//! - `axis` is the exact direction of the oriented frame's up (local `Z`);
62//! absent, it is `(0, 0, 1)`, the base frame's up.
63//! - `ref_direction` fixes the oriented frame's tangent role (local `X`);
64//! absent, it is `(1, 0, 0)`, the curve tangent.
65//! - The frame is orthonormalised by Gram-Schmidt with the axis primary:
66//! `up' = axis / |axis|`, `tangent' = normalise(r - (r . up') up')` for
67//! the unit `r = ref_direction / |ref_direction|`, and `lateral' = up' x
68//! tangent'`, so `(tangent', lateral', up')` stays right-handed.
69//! - A zero or non-finite vector, and an axis and reference direction
70//! (given or defaulted) parallel or anti-parallel within
71//! [`ORIENTATION_TOLERANCE`] (the sine of the angle between them), are
72//! refused by name when the node is pushed.
73//!
74//! The oriented triad replaces the base one wherever a frame ORIENTS
75//! something: the frame a resolved station presents, and the plane a
76//! section's profile is placed in (profile `x` along `lateral'`, `y`
77//! along `up'`). The OFFSETS stay in the base frame: they locate the
78//! station's origin, which the orientation does not move.
79//!
80//! # Seams (#263)
81//!
82//! A basis curve made of pieces -- a polyline, a B-spline with a corner
83//! knot, an elevated curve whose profile's grade breaks, a banked curve
84//! whose cant or pivot law jumps -- has two frames where two pieces meet.
85//! A station within the arc-length tolerance of such a seam is ON it and
86//! reads the frame of one piece, chosen by [`SeamSide`]: the piece that
87//! starts there ([`SeamSide::Outgoing`], the default and what every curve
88//! evaluator reads) or the one that ends there ([`SeamSide::Incoming`]).
89//! An [`OrientedCurveStation`] carries the side ([`OrientedCurveStation::seam`]);
90//! a plain [`CurveStation`] reads the outgoing piece, and
91//! [`CurveStation::with_seam_side`] turns it into an oriented station in
92//! its base frame that reads the other one.
93//!
94//! A run of sections or offsets ([`Station`]s along one directrix) decides
95//! most sides itself: a section standing exactly on an interior seam where
96//! the tangent turns is cut in the mitre plane, the bisector of the
97//! incoming and outgoing tangents, and so is the run where it crosses such
98//! a seam between two of its stations. The run's first station reads the
99//! outgoing piece and its last the incoming one, the pieces the run lies
100//! on. A seam where the tangent does not turn keeps the outgoing frame,
101//! unless the station standing on it names the other side (#301, below).
102//!
103//! # Seam-snapping window (#294)
104//!
105//! An authoring rule may put a station ON a seam when it lies within the
106//! model's precision of one -- IFC 4.3 ADD2 8.9.3.48.3 lets the previous
107//! segment's tangent govern a station "within precision limits" of a
108//! tangent discontinuity -- and that precision (up to a millimetre) is far
109//! wider than the arc-length tolerance. An [`OrientedCurveStation`]
110//! carries it as [`OrientedCurveStation::seam_window`], a non-negative
111//! distance in the basis's station measure, set by
112//! [`OrientedCurveStation::with_seam_window`]; `0`, the default, is the
113//! arc-length tolerance alone.
114//!
115//! A resolver snaps a station whose distance lies within the window of a
116//! seam of its basis whose frame may jump (a joint of a composite, a
117//! polyline vertex, a corner knot, a grade or cant seam) onto that seam,
118//! and reads it from the station's [`SeamSide`]; the station's point is
119//! then the seam's. Where a seam's distance is numerical (after an offset
120//! piece, an ellipse or a B-spline), the resolver uses a certified
121//! interval of it. It refuses by name a window holding two seams, and a
122//! seam whose certified interval straddles the window's edge (inside or
123//! outside cannot be told). The graph refuses a negative or non-finite
124//! window when the node is pushed. A plain [`CurveStation`] has no window.
125//!
126//! # Seam windows on run stations (#301)
127//!
128//! The stations of a run carry the same window and a [`SeamSide`]: a
129//! [`SectionAtStation`] (in [`SolidOperation::SectionsAtStations`] and
130//! [`SurfaceRelation::OpenSectionsAtStations`]) through
131//! [`SectionAtStation::with_seam_window`] and
132//! [`SectionAtStation::with_seam_side`], an offset's [`RunStation`] (in
133//! [`CurveRelation::OffsetsAtStations`]) likewise. Defaults are a window
134//! of `0` and `Outgoing`, today's reading. [`Station`],
135//! [`StationedSection`] and [`StationedOpenSection`] have public fields
136//! and stay window-free, so [`SolidOperation::StationedSpine`],
137//! [`SurfaceRelation::SectionedSurface`] and
138//! [`CurveRelation::OffsetByStations`] read as before; their general forms
139//! above carry windows.
140//!
141//! - **Snapping.** Each windowed station is snapped by the #294 rule
142//! against the run's directrix: onto the one seam whose frame may jump
143//! within its window, at the seam's own (certified) distance; a window
144//! holding two seams, or a seam whose certified position straddles its
145//! edge, is refused by name. The run then uses the snapped distance
146//! everywhere: the section's place, the linear interpolation between
147//! stations, the run's span. Snapped distances must still increase
148//! strictly; two stations snapped onto one seam, or one snapped past its
149//! neighbour, are refused by name.
150//! - **Side.** The run's first station reads the outgoing piece and its
151//! last the incoming one, whatever side they name: the run lies on those
152//! pieces, as a curve's start and end have one piece. An interior
153//! station on a seam where the tangent turns stands in the mitre plane,
154//! so its section (or offset point) is the mitre section, placed in BOTH
155//! pieces' frames and projected: its side is not read. An interior
156//! station on a seam where the tangent does not turn but the frame may
157//! jump (a banked curve's roll) is placed in the frame of the piece its
158//! side names.
159//!
160//! # Run stations at the basis's ends (#303)
161//!
162//! An offset by distances often continues its first and last offsets
163//! unchanged out to its basis's ends (IFC 4.3 ADD2 8.9.3.42.3), so its
164//! run needs a station AT the basis's end. Where the basis's length has
165//! no closed form (an offset beside a gradient curve, a clothoid or a
166//! B-spline, a B-spline itself), a caller without a provider cannot state
167//! that distance. A [`RunStation`] may therefore stand at the basis's
168//! start or end without stating it ([`RunStation::at_basis_start`],
169//! [`RunStation::at_basis_end`]): its [`RunStation::basis_end`] names
170//! which, and its stated distance is `0` and not read.
171//!
172//! - **Resolution.** A resolver reads the start as distance `0`, where the
173//! station measure starts on every curve, and the end as the basis's
174//! length by the kernel's own measure of it, the number a curve path's
175//! open end resolves to (#298): the length of the flattened basis
176//! (`axiolid-mesh-compile`'s `station::curve_path`) as
177//! `axiolid-evaluate`'s `CompositeBasis` measures it, bitwise; on an
178//! unbounded line its parameter domain end, `|direction|`, as inside a
179//! composite. A station at the end is then the same station stated at
180//! that number.
181//! - **Order.** The graph refuses, when the node is pushed, a station at
182//! the start that is not the run's first, one at the end that is not its
183//! last, and either stating a distance other than `0`; the stated
184//! distances must increase strictly from a start station's `0`. Whether
185//! the end comes after the last stated station needs the length, so a
186//! resolver refuses by name a run whose resolved distances do not
187//! increase strictly (a stated station at or past the basis's end).
188//! - **Windows.** Snapping (#301) applies after resolution: a station at
189//! the end with a window snaps onto the one seam within the window of
190//! the resolved end, by the #294 rule.
191//!
192//! Only the stations of a [`CurveRelation::OffsetsAtStations`] carry it;
193//! a [`SectionAtStation`] states its distance.
194//!
195//! [`SolidOperation::SectionsAtStations`]: crate::SolidOperation::SectionsAtStations
196//! [`SolidOperation::StationedSpine`]: crate::SolidOperation::StationedSpine
197//! [`SurfaceRelation::OpenSectionsAtStations`]: crate::SurfaceRelation::OpenSectionsAtStations
198//! [`SurfaceRelation::SectionedSurface`]: crate::SurfaceRelation::SectionedSurface
199//! [`CurveRelation::OffsetsAtStations`]: crate::CurveRelation::OffsetsAtStations
200//! [`CurveRelation::OffsetByStations`]: crate::CurveRelation::OffsetByStations
201//!
202//! # Placing a node at a station (#264)
203//!
204//! [`InstanceAtStation`] reuses a node -- a curve, a solid, a surface --
205//! in the frame of an [`OrientedCurveStation`], the way [`Instance`]
206//! reuses one under a fixed transform. The station stays symbolic: a
207//! kernel resolves its frame when it evaluates the node, with the
208//! station's [`StationFrame`], orientation and [`SeamSide`]. The source's
209//! local axes map onto the station's oriented frame as a linear placement
210//! reads them (buildingSMART IFC 4.3, `IfcAxis2PlacementLinear`):
211//!
212//! - local `x` onto the oriented tangent (`tangent'`),
213//! - local `y` onto the oriented lateral, to the LEFT (`lateral'`),
214//! - local `z` onto the oriented up (`up' = tangent' x lateral'`),
215//! - the local origin onto the station's point, its offsets read in the
216//! base frame.
217//!
218//! This is a rigid motion (right-handed, no scale). It is not the profile
219//! mapping above (a section's `x` along lateral and `y` along up), and not
220//! the provider layout a resolved station presents (`x` tangent, `y` up,
221//! `z` right): a placed curve is laid ALONG the curve, a section ACROSS
222//! it. A 2D source curve lies in its local `z = 0`, so it is placed in the
223//! plane of the oriented tangent and lateral, and the placed curve is a 3D
224//! curve whatever the source's dimension.
225//!
226//! [`Instance`]: crate::Instance
227//!
228//! # Sections between stations
229//!
230//! A profile placed at a station maps its `x` onto the lateral axis and its
231//! `y` onto up, so its normal `x x y` is the tangent. Between two stations
232//! everything is interpolated LINEARLY in distance: the offsets, each
233//! section point with its counterpart in the next section, and an explicit
234//! orientation (the unit axis and the unit reference direction,
235//! component-wise in the base frame, an absent one counting as `(0, 0, 1)`
236//! and `(1, 0, 0)`, then orthonormalised as above; a resolver refuses by
237//! name an interpolated pair that degenerates).
238//!
239//! # Matching points between sections
240//!
241//! Untagged sections are matched by position: a closed profile's points by
242//! ring and vertex index, an open section's by vertex index. Tagged
243//! sections ([`SectionAtStation::tags`], [`StationedOpenSection::tags`])
244//! are matched by tag. One run of sections is tagged throughout or not at
245//! all, and every section carries the same SET of tags, none repeated, so
246//! the tags are a bijection between any two sections:
247//!
248//! - an open section's tags run along its polyline in the first section's
249//! order or in reverse (a reversed section is joined reversed); any other
250//! order would cross the sheet and is refused;
251//! - a closed section's tags name its contour vertices, the outer ring's
252//! then each hole's, each ring from its first segment's start in its
253//! authored sense. Its profile must be polygonal (straight segments only,
254//! optionally under a `Profile::Derived` transform). The tags must map
255//! each ring of the first section onto one ring of every other, the
256//! outer onto the outer, in the same cyclic order once both are wound
257//! alike (outer counter-clockwise, holes clockwise): a section may start
258//! a ring at another vertex, list its holes in another order or wind a
259//! ring the other way, nothing else. A resolver checks this, since it
260//! needs the profile's vertices.
261
262use axiolid_core::{Scalar, Vec3};
263pub use axiolid_curve::SeamSide;
264
265use crate::NodeId;
266
267/// Offsets of a station from its basis curve, in the curve's section frame.
268#[derive(Debug, Clone, Copy, PartialEq, Default)]
269#[cfg_attr(
270 feature = "serde",
271 derive(serde::Serialize, serde::Deserialize),
272 serde(deny_unknown_fields)
273)]
274pub struct StationOffsets {
275 /// Along the lateral axis, positive to the left of the tangent.
276 pub lateral: Scalar,
277 /// Along the section's up axis.
278 pub vertical: Scalar,
279 /// Along the tangent, positive in the direction of increasing distance.
280 pub longitudinal: Scalar,
281}
282
283impl StationOffsets {
284 /// No offset: the station is on the curve.
285 pub const ZERO: Self = Self::new(0.0, 0.0, 0.0);
286
287 /// Offsets along the lateral, up and tangent axes.
288 #[must_use]
289 pub const fn new(lateral: Scalar, vertical: Scalar, longitudinal: Scalar) -> Self {
290 Self {
291 lateral,
292 vertical,
293 longitudinal,
294 }
295 }
296
297 /// Whether every offset is finite.
298 #[must_use]
299 pub fn is_finite(&self) -> bool {
300 self.lateral.is_finite() && self.vertical.is_finite() && self.longitudinal.is_finite()
301 }
302}
303
304/// A distance along a basis curve with offsets in its section frame.
305#[derive(Debug, Clone, Copy, PartialEq, Default)]
306#[cfg_attr(
307 feature = "serde",
308 derive(serde::Serialize, serde::Deserialize),
309 serde(deny_unknown_fields)
310)]
311pub struct Station {
312 /// Distance from the basis curve's start, in its convention (see the
313 /// [module documentation](self)).
314 pub distance: Scalar,
315 /// Offsets in the section frame at that distance.
316 pub offsets: StationOffsets,
317}
318
319impl Station {
320 /// A station at `distance` with `offsets`.
321 #[must_use]
322 pub const fn new(distance: Scalar, offsets: StationOffsets) -> Self {
323 Self { distance, offsets }
324 }
325
326 /// A station on the curve at `distance`.
327 #[must_use]
328 pub const fn at(distance: Scalar) -> Self {
329 Self::new(distance, StationOffsets::ZERO)
330 }
331
332 /// Whether the distance is finite and not negative and every offset is
333 /// finite.
334 #[must_use]
335 pub fn is_well_formed(&self) -> bool {
336 self.distance.is_finite() && self.distance >= 0.0 && self.offsets.is_finite()
337 }
338}
339
340/// Which frame a station's offsets and orientation are read in.
341#[non_exhaustive]
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
343#[cfg_attr(
344 feature = "serde",
345 derive(serde::Serialize, serde::Deserialize),
346 serde(deny_unknown_fields)
347)]
348pub enum StationFrame {
349 /// The basis curve's section frame: rolled on a banked curve, leaning
350 /// with the grade on any 3D curve.
351 #[default]
352 Section,
353 /// The vertical frame: the tangent's horizontal projection, the
354 /// horizontal left normal and `+Z`. Grade and bank are dropped, so a
355 /// section stands upright.
356 Plan,
357}
358
359/// A point and frame at a station along a basis curve.
360///
361/// Resolving it is curve evaluation, which a kernel does; see the
362/// [module documentation](self) for the conventions.
363#[derive(Debug, Clone, Copy, PartialEq)]
364#[cfg_attr(
365 feature = "serde",
366 derive(serde::Serialize, serde::Deserialize),
367 serde(deny_unknown_fields)
368)]
369pub struct CurveStation {
370 /// Basis curve, 2D or 3D.
371 pub basis: NodeId,
372 /// Distance and offsets.
373 pub station: Station,
374 /// Frame the offsets are read in, and that the station presents.
375 pub frame: StationFrame,
376}
377
378impl CurveStation {
379 /// A station along `basis` in the basis curve's section frame.
380 #[must_use]
381 pub const fn new(basis: NodeId, station: Station) -> Self {
382 Self {
383 basis,
384 station,
385 frame: StationFrame::Section,
386 }
387 }
388
389 /// This station, unturned, reading `side` of a seam it lies on (#263).
390 ///
391 /// A plain curve station always reads the outgoing piece; the side is
392 /// carried by an [`OrientedCurveStation`] whose orientation is the base
393 /// frame, so the node to push is
394 /// [`GeometryNode::OrientedCurveStation`](crate::GeometryNode::OrientedCurveStation).
395 #[must_use]
396 pub const fn with_seam_side(self, side: SeamSide) -> OrientedCurveStation {
397 OrientedCurveStation::new(self, StationOrientation::new(None, None)).with_seam_side(side)
398 }
399}
400
401/// A closed profile standing at a station along a sectioned spine's
402/// directrix.
403#[derive(Debug, Clone, Copy, PartialEq)]
404#[cfg_attr(
405 feature = "serde",
406 derive(serde::Serialize, serde::Deserialize),
407 serde(deny_unknown_fields)
408)]
409pub struct StationedSection {
410 /// Area profile node.
411 pub profile: NodeId,
412 /// Distance along the directrix and offsets of the profile's origin.
413 pub station: Station,
414}
415
416/// An open section standing at a station along a sectioned surface's
417/// directrix, its points named by tags.
418///
419/// `tags` names the vertices of the section's polyline in order; two
420/// consecutive sections are joined point to point by equal tags. Every
421/// section of one surface carries the same tags, in the first section's
422/// order or in reverse, or none (then the vertices are joined by index);
423/// see the [module documentation](self).
424#[derive(Debug, Clone, PartialEq)]
425#[cfg_attr(
426 feature = "serde",
427 derive(serde::Serialize, serde::Deserialize),
428 serde(deny_unknown_fields)
429)]
430pub struct StationedOpenSection {
431 /// Open profile node.
432 pub profile: NodeId,
433 /// One tag per vertex, or empty.
434 pub tags: Vec<String>,
435 /// Distance along the directrix and offsets of the profile's origin.
436 pub station: Station,
437}
438
439/// Largest sine of the angle between a [`StationOrientation`]'s axis and
440/// reference direction that still counts as parallel.
441pub const ORIENTATION_TOLERANCE: Scalar = 1e-9;
442
443/// An explicit in-section orientation at a station (#246).
444///
445/// Both vectors are components in the station's base frame, `(tangent,
446/// lateral, up)`; see the [module documentation](self) for their meaning,
447/// defaults and orthonormalisation. Both absent is the base frame itself,
448/// the [`Default`].
449#[non_exhaustive]
450#[derive(Debug, Clone, Copy, PartialEq, Default)]
451#[cfg_attr(
452 feature = "serde",
453 derive(serde::Serialize, serde::Deserialize),
454 serde(deny_unknown_fields)
455)]
456pub struct StationOrientation {
457 /// The oriented frame's up (local `Z`), exact; `None` for `(0, 0, 1)`.
458 pub axis: Option<Vec3>,
459 /// The direction the oriented frame's tangent role (local `X`) is
460 /// taken from, made perpendicular to the axis; `None` for `(1, 0, 0)`.
461 pub ref_direction: Option<Vec3>,
462}
463
464impl StationOrientation {
465 /// An orientation from an optional axis and reference direction, both
466 /// in the base frame.
467 #[must_use]
468 pub const fn new(axis: Option<Vec3>, ref_direction: Option<Vec3>) -> Self {
469 Self {
470 axis,
471 ref_direction,
472 }
473 }
474
475 /// Whether neither vector is given, so the base frame is used as is.
476 #[must_use]
477 pub const fn is_base(&self) -> bool {
478 self.axis.is_none() && self.ref_direction.is_none()
479 }
480
481 /// The unit axis and unit reference direction, defaults filled in.
482 ///
483 /// # Errors
484 ///
485 /// Why there are none, worded for
486 /// [`GraphError::InvalidStation`](crate::GraphError::InvalidStation):
487 /// a zero or non-finite vector, or the two parallel or anti-parallel
488 /// within [`ORIENTATION_TOLERANCE`].
489 pub fn unit_axes(&self) -> Result<(Vec3, Vec3), &'static str> {
490 let unit = |vector: Vec3, what: &'static str| {
491 let length = vector.length();
492 if vector.is_finite() && length.is_finite() && length > 1e-12 {
493 Ok(vector / length)
494 } else {
495 Err(what)
496 }
497 };
498 let axis = unit(
499 self.axis.unwrap_or(Vec3::Z),
500 "the orientation's axis is zero or not finite",
501 )?;
502 let reference = unit(
503 self.ref_direction.unwrap_or(Vec3::X),
504 "the orientation's reference direction is zero or not finite",
505 )?;
506 if axis.cross(reference).length() <= ORIENTATION_TOLERANCE {
507 return Err("the orientation's axis and reference direction are parallel");
508 }
509 Ok((axis, reference))
510 }
511}
512
513/// A [`CurveStation`] with an explicit orientation (#246) and the side of
514/// a seam it reads (#263).
515///
516/// Its point is the station's, offsets read in the base frame; its frame
517/// is the base frame turned by `orientation` (see the
518/// [module documentation](self)). On a seam of the basis curve the base
519/// frame is the one of the piece `seam` names.
520#[non_exhaustive]
521#[derive(Debug, Clone, Copy, PartialEq)]
522#[cfg_attr(
523 feature = "serde",
524 derive(serde::Serialize, serde::Deserialize),
525 serde(deny_unknown_fields)
526)]
527pub struct OrientedCurveStation {
528 /// The basis curve, distance, offsets and base frame.
529 pub station: CurveStation,
530 /// The axis and reference direction, in the base frame.
531 pub orientation: StationOrientation,
532 /// The piece read when the station lies on a seam of its basis curve;
533 /// [`SeamSide::Outgoing`] unless set by [`Self::with_seam_side`].
534 pub seam: SeamSide,
535 /// The seam-snapping window (#294): a station within this distance of
536 /// a seam of its basis is read ON that seam, from the side `seam`
537 /// names. `0` unless set by [`Self::with_seam_window`], which is the
538 /// arc-length tolerance alone (see the
539 /// [module documentation](self#seam-snapping-window-294)).
540 ///
541 /// Format 1.1 of the graph's wire format (ADR 0085): omitted when it
542 /// is `+0.0`, and an absent window reads as `0`.
543 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "no_window"))]
544 pub seam_window: Scalar,
545}
546
547/// Whether a seam window is the default `+0.0`, which the wire format
548/// omits (ADR 0085, format 1.1): a graph without windows writes the 1.0
549/// graph.
550#[cfg(feature = "serde")]
551// serde's `skip_serializing_if` passes the field by reference.
552#[allow(clippy::trivially_copy_pass_by_ref)]
553fn no_window(window: &Scalar) -> bool {
554 window.to_bits() == 0
555}
556
557/// Whether a run station's seam side is the default `Outgoing`, which the
558/// wire format omits (ADR 0085, format 1.2): a run without sides writes
559/// the 1.1 graph.
560#[cfg(feature = "serde")]
561// serde's `skip_serializing_if` passes the field by reference.
562#[allow(clippy::trivially_copy_pass_by_ref)]
563fn outgoing(side: &SeamSide) -> bool {
564 *side == SeamSide::Outgoing
565}
566
567impl OrientedCurveStation {
568 /// `station` turned by `orientation`, reading the outgoing piece on a
569 /// seam.
570 #[must_use]
571 pub const fn new(station: CurveStation, orientation: StationOrientation) -> Self {
572 Self {
573 station,
574 orientation,
575 seam: SeamSide::Outgoing,
576 seam_window: 0.0,
577 }
578 }
579
580 /// The same station reading `side` of a seam it lies on.
581 #[must_use]
582 pub const fn with_seam_side(mut self, side: SeamSide) -> Self {
583 self.seam = side;
584 self
585 }
586
587 /// The same station snapping onto a seam of its basis within `window`
588 /// (#294), in the basis's station measure: a station that close to
589 /// one seam is read on it, from its [`SeamSide`]. `0` is no window
590 /// beyond the arc-length tolerance. A negative or non-finite window is
591 /// refused when the node is pushed (see the
592 /// [module documentation](self#seam-snapping-window-294)).
593 #[must_use]
594 pub const fn with_seam_window(mut self, window: Scalar) -> Self {
595 self.seam_window = window;
596 self
597 }
598}
599
600impl From<CurveStation> for OrientedCurveStation {
601 /// The station in its base frame, reading the outgoing piece on a seam:
602 /// what the plain station means.
603 fn from(station: CurveStation) -> Self {
604 Self::new(station, StationOrientation::default())
605 }
606}
607
608/// A section standing at a station, with optional tags and an optional
609/// explicit orientation (#246): the general form of [`StationedSection`]
610/// (closed) and [`StationedOpenSection`] (open).
611///
612/// Built with [`SectionAtStation::new`] and the `with_` methods, so that
613/// fields can be added without breaking callers.
614#[non_exhaustive]
615#[derive(Debug, Clone, PartialEq)]
616#[cfg_attr(
617 feature = "serde",
618 derive(serde::Serialize, serde::Deserialize),
619 serde(deny_unknown_fields)
620)]
621pub struct SectionAtStation {
622 /// The profile node: an area profile in a sectioned spine, an open
623 /// profile in a sectioned surface.
624 pub profile: NodeId,
625 /// Distance along the directrix and offsets of the profile's origin,
626 /// in the base frame.
627 pub station: Station,
628 /// One tag per vertex, or empty to match by index; see the
629 /// [module documentation](self).
630 pub tags: Vec<String>,
631 /// The section's orientation in the base frame; the default is the
632 /// base frame.
633 pub orientation: StationOrientation,
634 /// The piece read where the section stands on a seam its run does not
635 /// decide (#301): [`SeamSide::Outgoing`] unless set by
636 /// [`Self::with_seam_side`]. See the
637 /// [module documentation](self#seam-windows-on-run-stations-301) for
638 /// where a run reads it.
639 ///
640 /// Format 1.2 of the graph's wire format (ADR 0085): omitted when it
641 /// is `Outgoing`, and an absent side reads as `Outgoing`.
642 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "outgoing"))]
643 pub seam: SeamSide,
644 /// The seam-snapping window (#294, #301): a section within this
645 /// distance of one seam of the directrix stands ON that seam. `0`
646 /// unless set by [`Self::with_seam_window`], which is the arc-length
647 /// tolerance alone.
648 ///
649 /// Format 1.2 of the graph's wire format (ADR 0085): omitted when it
650 /// is `+0.0`, and an absent window reads as `0`.
651 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "no_window"))]
652 pub seam_window: Scalar,
653}
654
655impl SectionAtStation {
656 /// `profile` at `station`, untagged, in the base frame.
657 #[must_use]
658 pub const fn new(profile: NodeId, station: Station) -> Self {
659 Self {
660 profile,
661 station,
662 tags: Vec::new(),
663 orientation: StationOrientation::new(None, None),
664 seam: SeamSide::Outgoing,
665 seam_window: 0.0,
666 }
667 }
668
669 /// The same section with `tags`, one per vertex.
670 #[must_use]
671 pub fn with_tags<I, T>(mut self, tags: I) -> Self
672 where
673 I: IntoIterator<Item = T>,
674 T: Into<String>,
675 {
676 self.tags = tags.into_iter().map(Into::into).collect();
677 self
678 }
679
680 /// The same section turned by `orientation`.
681 #[must_use]
682 pub const fn with_orientation(mut self, orientation: StationOrientation) -> Self {
683 self.orientation = orientation;
684 self
685 }
686
687 /// The same section reading `side` of a seam it stands on, where its
688 /// run does not decide the side (#301; see the
689 /// [module documentation](self#seam-windows-on-run-stations-301)).
690 #[must_use]
691 pub const fn with_seam_side(mut self, side: SeamSide) -> Self {
692 self.seam = side;
693 self
694 }
695
696 /// The same section snapping onto a seam of the directrix within
697 /// `window` (#301), in the directrix's station measure, by the #294
698 /// rule. `0` is no window beyond the arc-length tolerance; a negative
699 /// or non-finite window is refused when the node is pushed.
700 #[must_use]
701 pub const fn with_seam_window(mut self, window: Scalar) -> Self {
702 self.seam_window = window;
703 self
704 }
705}
706
707impl From<StationedSection> for SectionAtStation {
708 fn from(section: StationedSection) -> Self {
709 Self::new(section.profile, section.station)
710 }
711}
712
713impl From<StationedOpenSection> for SectionAtStation {
714 fn from(section: StationedOpenSection) -> Self {
715 Self::new(section.profile, section.station).with_tags(section.tags)
716 }
717}
718
719/// Which end of its basis a [`RunStation`] stands at, measured by the
720/// kernel (#303; see the
721/// [module documentation](self#run-stations-at-the-basiss-ends-303)).
722#[non_exhaustive]
723#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
724#[cfg_attr(
725 feature = "serde",
726 derive(serde::Serialize, serde::Deserialize),
727 serde(deny_unknown_fields)
728)]
729pub enum BasisEnd {
730 /// The basis's start: distance `0`.
731 Start,
732 /// The basis's end: its length, by the kernel's own measure.
733 End,
734}
735
736/// A station of an offset run with the side of a seam it reads and its
737/// seam-snapping window (#301): the general form of the plain [`Station`]
738/// a [`CurveRelation::OffsetByStations`] takes, used by
739/// [`CurveRelation::OffsetsAtStations`]. It may also stand at its basis's
740/// start or end, its distance measured by the kernel (#303).
741///
742/// Built with [`RunStation::new`], [`RunStation::at_basis_start`],
743/// [`RunStation::at_basis_end`] and the `with_` methods, so that fields can
744/// be added without breaking callers. See the
745/// [module documentation](self#seam-windows-on-run-stations-301) for how a
746/// run reads the side and the window, and
747/// [its #303 section](self#run-stations-at-the-basiss-ends-303) for the
748/// basis's ends.
749///
750/// [`CurveRelation::OffsetByStations`]: crate::CurveRelation::OffsetByStations
751/// [`CurveRelation::OffsetsAtStations`]: crate::CurveRelation::OffsetsAtStations
752#[non_exhaustive]
753#[derive(Debug, Clone, Copy, PartialEq)]
754#[cfg_attr(
755 feature = "serde",
756 derive(serde::Serialize, serde::Deserialize),
757 serde(deny_unknown_fields)
758)]
759pub struct RunStation {
760 /// Distance along the basis and offsets in its frame there.
761 pub station: Station,
762 /// The piece read where the station stands on a seam its run does not
763 /// decide: [`SeamSide::Outgoing`] unless set by
764 /// [`Self::with_seam_side`]. Omitted from the wire format when it is
765 /// `Outgoing`.
766 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "outgoing"))]
767 pub seam: SeamSide,
768 /// The seam-snapping window (#294): a station within this distance of
769 /// one seam of the basis stands ON it. `0` unless set by
770 /// [`Self::with_seam_window`]. Omitted from the wire format when it is
771 /// `+0.0`.
772 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "no_window"))]
773 pub seam_window: Scalar,
774 /// The end of the basis the station stands at, its distance measured
775 /// by the kernel (#303): `None`, the default, for the stated
776 /// `station.distance`; otherwise that distance is `0` and not read
777 /// (see the
778 /// [module documentation](self#run-stations-at-the-basiss-ends-303)).
779 ///
780 /// Format 1.3 of the graph's wire format (ADR 0085): omitted when it
781 /// is `None`, and an absent end reads as `None`.
782 #[cfg_attr(
783 feature = "serde",
784 serde(default, skip_serializing_if = "Option::is_none")
785 )]
786 pub basis_end: Option<BasisEnd>,
787}
788
789impl RunStation {
790 /// `station`, reading the outgoing piece on a seam, with no window.
791 #[must_use]
792 pub const fn new(station: Station) -> Self {
793 Self {
794 station,
795 seam: SeamSide::Outgoing,
796 seam_window: 0.0,
797 basis_end: None,
798 }
799 }
800
801 /// A station at the basis's start with `offsets` (#303): distance `0`,
802 /// reading the outgoing piece, with no window.
803 #[must_use]
804 pub const fn at_basis_start(offsets: StationOffsets) -> Self {
805 let mut station = Self::new(Station::new(0.0, offsets));
806 station.basis_end = Some(BasisEnd::Start);
807 station
808 }
809
810 /// A station at the basis's end with `offsets` (#303), reading the
811 /// outgoing piece, with no window: its distance is the basis's length
812 /// by the kernel's own measure, which the caller need not know (see
813 /// the [module documentation](self#run-stations-at-the-basiss-ends-303)).
814 #[must_use]
815 pub const fn at_basis_end(offsets: StationOffsets) -> Self {
816 let mut station = Self::new(Station::new(0.0, offsets));
817 station.basis_end = Some(BasisEnd::End);
818 station
819 }
820
821 /// The same station reading `side` of a seam it stands on, where its
822 /// run does not decide the side.
823 #[must_use]
824 pub const fn with_seam_side(mut self, side: SeamSide) -> Self {
825 self.seam = side;
826 self
827 }
828
829 /// The same station snapping onto a seam of its basis within `window`,
830 /// in the basis's station measure, by the #294 rule. A negative or
831 /// non-finite window is refused when the node is pushed.
832 #[must_use]
833 pub const fn with_seam_window(mut self, window: Scalar) -> Self {
834 self.seam_window = window;
835 self
836 }
837}
838
839impl From<Station> for RunStation {
840 /// The plain station: the outgoing piece, no window, what a station of
841 /// an [`OffsetByStations`](crate::CurveRelation::OffsetByStations)
842 /// means.
843 fn from(station: Station) -> Self {
844 Self::new(station)
845 }
846}
847
848/// A node reused in the frame of an [`OrientedCurveStation`] (#264): the
849/// station-framed form of [`Instance`](crate::Instance).
850///
851/// The source's local `x`, `y`, `z` and origin are placed on the
852/// station's oriented tangent, left lateral, up and point (see the
853/// [module documentation](self#placing-a-node-at-a-station-264)); the
854/// frame is resolved at evaluation, reading the side of a seam the station
855/// names. A placed curve is a 3D curve, a placed solid or surface keeps
856/// its family. Built with [`InstanceAtStation::new`], so fields can be
857/// added without breaking callers.
858#[non_exhaustive]
859#[derive(Debug, Clone, Copy, PartialEq)]
860#[cfg_attr(
861 feature = "serde",
862 derive(serde::Serialize, serde::Deserialize),
863 serde(deny_unknown_fields)
864)]
865pub struct InstanceAtStation {
866 /// The reused node, in its own local coordinates.
867 pub source: NodeId,
868 /// The station whose oriented frame places it.
869 pub station: OrientedCurveStation,
870}
871
872impl InstanceAtStation {
873 /// `source` placed in the frame of `station`.
874 #[must_use]
875 pub const fn new(source: NodeId, station: OrientedCurveStation) -> Self {
876 Self { source, station }
877 }
878}