use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{
Axis, FeatureContext, FeatureResult, Frame, PortKind, PortRecord, SceneMap, ScenePoint,
};
use crate::{make_line, Vec3};
const DEFAULT_EXTENSION: f64 = 2.0;
const DEFAULT_DISPLAY_LENGTH: f64 = 6.0;
const MIN_DISPLAY_LENGTH: f64 = 0.1;
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
match build(ctx) {
Ok(result) => result,
Err(error) => ctx.fail(format!("port: {error}")),
}
}
fn build(ctx: &FeatureContext) -> Result<FeatureResult, String> {
let id = if ctx.id.is_empty() { "Port" } else { ctx.id.as_str() };
let mut result = FeatureResult::pass_through(ctx.id.clone(), ctx.feature_type.clone());
let transform = ctx.param("transform");
let position = common::vec3_from_value(
ctx.env,
transform.and_then(|t| t.get("position")),
"transform.position",
[0.0, 0.0, 0.0],
)?;
let rotation_deg = common::vec3_from_value(
ctx.env,
transform.and_then(|t| t.get("rotationEuler")),
"transform.rotationEuler",
[0.0, 0.0, 0.0],
)?;
let point = Vec3::new(position[0], position[1], position[2]);
let rotated_x = super::datum::rotate_euler_xyz(
Vec3::new(1.0, 0.0, 0.0),
[
rotation_deg[0].to_radians(),
rotation_deg[1].to_radians(),
rotation_deg[2].to_radians(),
],
);
let mut direction = rotated_x;
if let Some(name) = common::first_reference_name(ctx.param("directionRef")) {
match resolve_direction(ctx, &name)? {
Some(resolved) => direction = resolved,
None => result.unresolved.push(name),
}
}
let mut direction = direction
.normalized()
.map_err(|_| "direction is a zero vector".to_string())?;
if ctx
.param("reverseDirection")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
{
direction = direction.scale(-1.0);
}
let kind = match ctx.param("kind").and_then(serde_json::Value::as_str) {
Some("waypoint") => PortKind::Waypoint,
Some("termination") | None => PortKind::Termination,
Some(other) => return Err(format!("unknown port kind '{other}' (termination | waypoint)")),
};
let extension = common::optional_number(ctx, "extension")?
.unwrap_or(DEFAULT_EXTENSION)
.max(0.0);
let display_length = common::optional_number(ctx, "displayLength")?
.unwrap_or(DEFAULT_DISPLAY_LENGTH)
.max(MIN_DISPLAY_LENGTH);
let label = ctx
.param("portName")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|text| !text.is_empty())
.unwrap_or(id)
.to_string();
let record = PortRecord {
point,
direction,
kind,
extension,
display_length,
label,
};
publish_port(&mut result, id, record)?;
Ok(result)
}
pub(crate) fn port_line(record: &PortRecord) -> (Vec3, Vec3) {
let point = record.point;
let direction = record.direction;
let display_length = record.display_length;
match record.kind {
PortKind::Termination => (point, point.add(direction.scale(display_length))),
PortKind::Waypoint => (
point.sub(direction.scale(display_length * 0.5)),
point.add(direction.scale(display_length * 0.5)),
),
}
}
pub(crate) fn publish_port(
result: &mut FeatureResult,
id: &str,
record: PortRecord,
) -> Result<(), String> {
let (line_start, line_end) = port_line(&record);
result
.paths
.push((format!("{id}:PortLine"), vec![make_line(line_start, line_end)?]));
result
.points
.push((format!("{id}:Base"), ScenePoint::model(record.point)));
result.axes.push((
id.to_string(),
Axis { point: record.point, direction: record.direction },
));
result.frames.push((
id.to_string(),
Frame::from_origin_normal(record.point, record.direction)?,
));
result.ports.push((id.to_string(), record));
Ok(())
}
pub(crate) fn place_scene_port(
scene: &mut SceneMap,
id: &str,
record: PortRecord,
) -> Result<(), String> {
let (line_start, line_end) = port_line(&record);
scene
.paths
.insert(format!("{id}:PortLine"), vec![make_line(line_start, line_end)?]);
scene
.points
.insert(format!("{id}:Base"), ScenePoint::model(record.point));
scene.axes.insert(
id.to_string(),
Axis { point: record.point, direction: record.direction },
);
scene.frames.insert(
id.to_string(),
Frame::from_origin_normal(record.point, record.direction)?,
);
scene.ports.insert(id.to_string(), record);
Ok(())
}
fn resolve_direction(ctx: &FeatureContext, name: &str) -> Result<Option<Vec3>, String> {
if let Some(face) = ctx.scene.resolve_face(name) {
let (_, normal) = common::face_point_normal(face)?;
return Ok(Some(normal));
}
if let Some(frame) = ctx.scene.resolve_frame(name) {
return Ok(Some(frame.z_axis));
}
if let Some(axis) = ctx.scene.resolve_axis(name) {
return Ok(Some(axis.direction));
}
if let Some(edge) = ctx.scene.resolve_edge(name) {
return Ok(Some(common::edge_axis(edge)?.direction));
}
Ok(None)
}
pub fn context_applicable(probe: &crate::feature_pipeline::SelectionProbe) -> bool {
probe.faces > 0 || probe.edges > 0
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "PORT",
"shortName": "PORT",
"longName": "Port",
"displayBuilder": true,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "Unique identifier for the port feature."
},
"portName": {
"type": "string",
"default_value": "Port",
"label": "Port Name",
"hint": "Logical name for this electrical termination or waypoint (the harness panel's endpoint label)."
},
"kind": {
"type": "options",
"options": [
"termination",
"waypoint"
],
"default_value": "termination",
"label": "Port Type",
"hint": "Termination ports are cable endpoints. Waypoint ports join spline paths into a network; a wire enters on one side and leaves on the other."
},
"directionRef": {
"type": "reference_selection",
"selectionFilter": [
"FACE",
"EDGE",
"PLANE",
"DATUM"
],
"multiple": false,
"default_value": null,
"label": "Direction Reference",
"hint": "Optional geometry that sets the port direction: a face normal, an edge direction or a plane normal. Without it the placement rotation's X axis is the direction."
},
"transform": {
"type": "transform",
"default_value": {
"position": [
0,
0,
0
],
"rotationEuler": [
0,
0,
0
],
"scale": [
1,
1,
1
]
},
"label": "Placement",
"hint": "The port's base point (position) and, without a direction reference, its direction (the rotated X axis)."
},
"reverseDirection": {
"type": "boolean",
"default_value": false,
"label": "Reverse Direction",
"hint": "Flip the port direction vector."
},
"extension": {
"type": "number",
"default_value": 2,
"label": "Extension",
"hint": "How far the wire stays straight from the base point before bending. An attached spline anchor takes it as its forward and backward distance."
},
"displayLength": {
"type": "number",
"default_value": 6,
"label": "Display Length",
"hint": "Length of the drawn port line."
}
}
})
}