///|
/// ComponentLayer is one solid body of a component, expressed in the
/// component's local frame.
///
/// Many mechanically correct parts are naturally built from stacked bodies
/// (for example a screw head with a blind socket is a socket ring over a floor
/// disc). Keeping the layers explicit makes that construction visible instead
/// of pretending a nominal boolean trimmed it.
pub(all) struct ComponentLayer {
shape : @cad.Shape
name : String
color : @cad.Rgb
placement : Placement
}
///|
/// Component is a named, metadata-bearing collection of placed solid layers in
/// a local millimetre frame.
///
/// Components are immutable values: `translate`, `rotate_x`, and friends return
/// a new component. `nodes()` materialises the layers as `@cad.SceneNode`
/// values for a `Design`, preserving order and names.
pub struct Component {
priv name : String
priv metadata : Metadata
priv layers : Array[ComponentLayer]
priv description : String
priv features : FeatureRecord
priv connectors : Array[MateConnector]
}
///|
/// Component::new builds an empty component with a name and metadata.
///
/// `features` is the machine-readable feature record. When omitted it is empty
/// and `documentation()` states no modelled features.
pub fn Component::new(
name~ : String,
metadata~ : Metadata,
description~ : String = "",
features? : FeatureRecord,
) -> Component {
let record = match features {
Some(f) => f
None => FeatureRecord::empty()
}
{ name, metadata, layers: [], description, features: record, connectors: [], }
}
///|
/// Component::add_layer appends one solid body. Layers are evaluated in the
/// order they were added.
pub fn Component::add_layer(
self : Component,
shape : @cad.Shape,
name~ : String = "",
color~ : @cad.Rgb = @cad.Rgb::orange(),
placement~ : Placement = Placement::identity(),
) -> Component {
let layer_name = if name == "" {
"\{self.name}/layer-\{self.layers.length()}"
} else {
"\{self.name}/\{name}"
}
let layers = self.layers.copy()
layers.push({ shape, name: layer_name, color, placement, })
{ ..self, layers, }
}
///|
/// Component::add_layers appends every layer in `layers`.
pub fn Component::add_layers(
self : Component,
layers : Array[ComponentLayer],
) -> Component {
let out = self.layers.copy()
out.push_iter(layers.iter())
{ ..self, layers: out, }
}
///|
/// Component::name returns the component's canonical name.
pub fn Component::name(self : Component) -> String {
self.name
}
///|
/// Component::metadata returns the component metadata.
pub fn Component::metadata(self : Component) -> Metadata {
self.metadata
}
///|
/// Component::description returns the free-form component description.
pub fn Component::description(self : Component) -> String {
self.description
}
///|
/// Component::features returns the machine-readable feature record.
pub fn Component::features(self : Component) -> FeatureRecord {
self.features
}
///|
/// Component::with_features returns a copy with a new feature record.
pub fn Component::with_features(
self : Component,
features : FeatureRecord,
) -> Component {
{ ..self, features, }
}
///|
/// Component::layers returns a copy of the component layers in local
/// coordinates.
pub fn Component::layers(self : Component) -> Array[ComponentLayer] {
self.layers.copy()
}
///|
/// Component::with_connector registers a named mate connector.
///
/// The component is immutable; a successful call returns a copy with one more
/// connector. Invalid connectors and duplicate names are rejected with a typed
/// error. Registering a connector never changes layers, geometry, or metadata.
pub fn Component::with_connector(
self : Component,
connector : MateConnector,
) -> Result[Component, MateError] {
match connector.validate() {
Err(e) => return Err(e)
Ok(_) => ()
}
for existing in self.connectors {
if existing.name == connector.name {
return Err(DuplicateConnector(connector.name))
}
}
let connectors = self.connectors.copy()
connectors.push(connector)
Ok({ ..self, connectors, })
}
///|
/// Component::with_connectors registers several connectors, rejecting the first
/// invalid or duplicate name. Nothing is changed when any connector fails.
pub fn Component::with_connectors(
self : Component,
connectors : Array[MateConnector],
) -> Result[Component, MateError] {
let mut component = self
for connector in connectors {
match component.with_connector(connector) {
Ok(next) => component = next
Err(e) => return Err(e)
}
}
Ok(component)
}
///|
/// Component::with_connector_unchecked registers a connector that the caller has
/// already constructed correctly.
///
/// Component builders use this for their own documented metadata, where a
/// failure means the builder itself is broken rather than the caller's
/// parameters. Caller-supplied connectors should use `with_connector`.
pub fn Component::with_connector_unchecked(
self : Component,
connector : MateConnector,
) -> Component {
match self.with_connector(connector) {
Ok(component) => component
Err(e) => abort(e.message())
}
}
///|
/// Component::connectors returns a copy of the component's mate connectors.
pub fn Component::connectors(self : Component) -> Array[MateConnector] {
self.connectors.copy()
}
///|
/// Component::connector looks up a connector by name.
pub fn Component::connector(
self : Component,
name : String,
) -> Result[MateConnector, MateError] {
for connector in self.connectors {
if connector.name == name {
return Ok(connector)
}
}
Err(UnknownConnector(name))
}
///|
/// Component::connector_placed returns a connector transformed by `parent`,
/// for snapping against a component that is already placed in an assembly.
pub fn Component::connector_placed(
self : Component,
name : String,
parent : Placement,
) -> Result[MateConnector, MateError] {
match self.connector(name) {
Ok(connector) => Ok(connector.transformed(parent))
Err(e) => Err(e)
}
}
///|
/// Component::rebase_to_connector applies the inverse of a named connector's
/// placement to every layer and every connector, making that connector frame the
/// component origin.
///
/// Only frames move. Layer geometry, volume, layer count, and shape identity are
/// unchanged, as are the relative positions of all connectors.
pub fn Component::rebase_to_connector(
self : Component,
name : String,
) -> Result[Component, MateError] {
let connector = match self.connector(name) {
Ok(c) => c
Err(e) => return Err(e)
}
let inv = Placement::inverse(connector.placement)
let layers = self.layers.map(fn(layer) {
{ ..layer, placement: Placement::compose(inv, layer.placement), }
})
let connectors = self.connectors.map(fn(c) { c.transformed(inv) })
Ok({ ..self, layers, connectors, })
}
///|
/// Component::with_name returns a copy with a new name.
///
/// Existing layer names are not rewritten; callers that need a fully consistent
/// name should build the component under the final name.
pub fn Component::with_name(self : Component, name : String) -> Component {
{ ..self, name, }
}
///|
/// Component::with_metadata returns a copy with new metadata.
pub fn Component::with_metadata(
self : Component,
metadata : Metadata,
) -> Component {
{ ..self, metadata, }
}
///|
/// Component::with_description returns a copy with a new description.
pub fn Component::with_description(
self : Component,
description : String,
) -> Component {
{ ..self, description, }
}
///|
/// Component::translate moves the whole component by `(x, y, z)` millimetres in
/// its parent frame.
pub fn Component::translate(
self : Component,
x~ : Double = 0.0,
y~ : Double = 0.0,
z~ : Double = 0.0,
) -> Component {
self.transform_with(Placement::translation(x, y, z))
}
///|
/// Component::rotate_x rotates the component about its local +X axis by
/// `angle` degrees.
pub fn Component::rotate_x(self : Component, angle : Double) -> Component {
self.transform_with(Placement::rotation_x(angle))
}
///|
/// Component::rotate_y rotates the component about its local +Y axis by
/// `angle` degrees.
pub fn Component::rotate_y(self : Component, angle : Double) -> Component {
self.transform_with(Placement::rotation_y(angle))
}
///|
/// Component::rotate_z rotates the component about its local +Z axis by
/// `angle` degrees.
pub fn Component::rotate_z(self : Component, angle : Double) -> Component {
self.transform_with(Placement::rotation_z(angle))
}
///|
/// Component::transform_with prepends `outer` to every layer placement.
pub fn Component::transform_with(
self : Component,
outer : Placement,
) -> Component {
let layers = self.layers.map(fn(layer) {
{ ..layer, placement: Placement::compose(outer, layer.placement), }
})
let connectors = self.connectors.map(fn(connector) {
connector.transformed(outer)
})
{ ..self, layers, connectors, }
}
///|
/// Component::nodes materialises the component in its own local frame.
pub fn Component::nodes(self : Component) -> Array[@cad.SceneNode] {
self.nodes_under(Placement::identity())
}
///|
/// Component::nodes_under materialises the component with `parent` applied to
/// every layer. This is the hook the assembly layer uses.
pub fn Component::nodes_under(
self : Component,
parent : Placement,
) -> Array[@cad.SceneNode] {
self.layers.map(fn(layer) {
let combined = Placement::compose(parent, layer.placement)
let (rx, ry, rz) = combined.to_euler()
layer.shape
.with_name(layer.name)
.with_color(layer.color)
.rotate_x(rx)
.rotate_y(ry)
.rotate_z(rz)
.translate(x=combined.trans.0, y=combined.trans.1, z=combined.trans.2)
})
}
///|
/// Component::bounding_box returns the world-axis-aligned bounds of the
/// materialised component in its local frame, as
/// `(x_min, x_max, y_min, y_max, z_min, z_max)`.
pub fn Component::bounding_box(
self : Component,
) -> (Double, Double, Double, Double, Double, Double) {
let nodes = self.nodes()
let mut x0 = 1.0e300
let mut x1 = -1.0e300
let mut y0 = 1.0e300
let mut y1 = -1.0e300
let mut z0 = 1.0e300
let mut z1 = -1.0e300
for node in nodes {
let (a0, a1, b0, b1, c0, c1) = node.bounding_box()
if a0 < x0 {
x0 = a0
}
if a1 > x1 {
x1 = a1
}
if b0 < y0 {
y0 = b0
}
if b1 > y1 {
y1 = b1
}
if c0 < z0 {
z0 = c0
}
if c1 > z1 {
z1 = c1
}
}
if nodes.is_empty() {
(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
} else {
(x0, x1, y0, y1, z0, z1)
}
}
///|
/// Component::size returns the bounding-box extent `(dx, dy, dz)`.
pub fn Component::size(self : Component) -> @cad.Vec3 {
let (x0, x1, y0, y1, z0, z1) = self.bounding_box()
(x1 - x0, y1 - y0, z1 - z0)
}
///|
/// Component::documentation returns the metadata plus any family description
/// and the machine-readable feature record, suitable for a STEP header.
pub fn Component::documentation(self : Component) -> String {
let meta = self.metadata.documentation()
let lines : Array[String] = [meta]
if self.description != "" {
lines.push("Notes: " + self.description)
}
lines.push(self.features.documentation())
lines.join("\n")
}
///|
/// Component::to_design materialises the component as a `@cad.Design`.
pub fn Component::to_design(
self : Component,
name? : String,
documentation? : String,
) -> @cad.Design {
let design_name = match name {
Some(n) => n
None => self.name
}
let doc = match documentation {
Some(d) => d + "\n" + self.documentation()
None => self.documentation()
}
@cad.Design::new(
name=design_name,
description=self.metadata.summary(),
documentation=doc,
).add_iter(self.nodes().iter())
}
///|
/// Component::to_step renders the component as flat STEP text.
pub fn Component::to_step(self : Component, checked~ : Bool = false) -> String {
let design = self.to_design()
if checked {
design.to_step_file_checked()
} else {
design.to_step_file()
}
}
///|
/// Component::write_step writes the component as a STEP file. Pass
/// `checked=true` to refuse designs with error diagnostics.
pub async fn Component::write_step(
self : Component,
out_path : String?,
checked~ : Bool = false,
) -> Unit {
self.to_design().write_step(out_path, checked~)
}