Networks and junctions
union() two hollow tubes and one tube’s wall seals the other’s bore at
the junction. A network is how several hollow runs meet with every bore left
open.
trunk = ut_run("trunk", ut_polyline([[-50,0,0], [50,0,0]]), ut_round(od = 20, wall = 2));
branch = ut_run("branch", ut_polyline([[0,10,0], [0,50,0]]), ut_round(od = 12, wall = 2));
ut_assemble(ut_net([trunk, branch],
[ut_joint([["trunk", ["s", 50]], ["branch", "a"]])]));
Runs, joints, nets
ut_run(name, path, profile, group = 0, opts = [])
ut_joint(incident, body = "ball", at = undef)
ut_net(runs, joints = [])
ut_assemble(net, opts = [])
A run is a named path with a profile. opts are frame options for that run
(frames.md). A split profile cannot be a run.
A joint lists the run ends that meet. Each entry is [runName, end], where
end is:
| end | meaning |
|---|---|
"a" |
the start of the run |
"b" |
the end of the run |
["s", x] |
a point x mm along the run. The run passes through. |
Joints are always declared. Nothing is inferred from two runs happening to touch, because coincidence cannot find a branch that ends inside a trunk.
The joint’s node is at if you give it. Otherwise it is the first mid-run
landing’s point on that run’s centreline, or failing that the mean of the
incident end positions.
Known issue (#35). An
endthat is none of the three forms is accepted and treated as an unjoined"b": the network validates, the end stays an open port and its bore is not opened into the joint.
Known issue (#36). A solid rod as an unjoined run, and two runs with the same name, both abort with a message that names the wrong cause.
How the bores stay open
For each lumen group, every shell is unioned first and every bore is cut second:
for (g = ut_groups(net))
difference() {
union() { shells of runs in g; joint shells in g }
union() { bores of runs in g; joint cores in g }
}
Because the cut comes after all the unions, no run’s wall can seal another’s bore. A run’s bore is the same path and frames as its shell, swept with the inner loop of the profile, so nothing is computed twice.
This is the only CSG in the library. Along a run, a tube is one swept polyhedron.
Ends are extended into the joint
Each joint-incident "a" or "b" end is pushed along its own end tangent:
| part | extended |
|---|---|
| shell | to the node |
| bore | half a core radius past the node |
So a branch can be drawn the way a tee is normally described, starting on the trunk’s outer wall, and the assembler carries it to the trunk’s centreline. Extending only the bore is not enough: a branch ending exactly on the trunk’s surface would touch it along a circle and overlap in nothing.
Joint bodies
| body | what it is |
|---|---|
"ball" (default) |
a sphere at the node as large as the largest incident outer reach, with a core sphere cut from it as large as the smallest incident bore’s inscribed circle |
"none" |
no sphere. The runs simply overlap. |
Both radii are measured from the incident profiles, so an arbitrary section needs no special case.
Lumen groups
Every run carries a group, default 0, and the union-then-cut runs once per
group. Runs that join must share a group. Runs that nest — a liner in a
jacket, a tube-in-tube exchanger — must be in different groups, or the outer
bore cuts the inner run away entirely:
jacket = ut_run("jacket", path, ut_round(od = 30, id = 26), group = 0);
liner = ut_run("liner", path, ut_round(od = 12, id = 8), group = 1);
ut_assemble(ut_net([jacket, liner]));
Measured with both in one group: 7883.8 of 29958.5 mm³, 26% of the material, gone, with CGAL reporting a valid solid.
What is checked
ut_assemble runs ut_check_net(net) first. All of it is arithmetic on the
paths and profiles; nothing is rendered.
| check | refuses |
|---|---|
NET-0 |
any run that fails the single-run checks (a bend radius not greater than the profile’s reach) |
NET-1 |
a run end incident to more than one joint |
NET-2 |
a joint with an incident run that has no bore |
NET-3 |
a run end farther from the node than its own reach plus the ball’s radius |
NET-4 |
a ball with no wall. Warns when the wall is under 1.2 mm. |
NET-5 |
two runs in one group where one fits inside the other’s bore and their centrelines come within that bore |
NET-6 |
a joint whose runs are in different groups |
Notes on two of them:
NET-4depends on resolution. Both spheres are faceted, and the wall is measured from the outer ball’s nearest face to the core’s vertices. Twood=20 id=17runs at$fn=5have a 3.1 mm wall by radius and −0.33 mm by facet, which is a hole. Raise$fnor thicken the wall.NET-5is sampled, 24 points per path, so a crossing that falls entirely between samples is not seen. Runs that share a joint are exempt: meeting at a joint necessarily brings them within a bore radius.
Ports
An open end is one that no joint claims. Ports are derived, not declared:
ports = ut_ports(net); // every open end
p = ut_port_named(net, "branch.b");
ut_at_port(p) my_fitting(); // +Z outward, +X along the roll reference
ut_port_marker(p); // debug arrows
A port is a full frame, not a position and a diameter:
| accessor | |
|---|---|
ut_port_name(p) |
"run.a" or "run.b" |
ut_port_pos(p) |
the end point |
ut_port_dir(p) |
unit vector pointing outward, away from the material |
ut_port_normal(p) |
the roll reference there, so a hex flat or lug lands repeatably |
ut_port_profile(p) |
the run’s profile |
ut_port_s(p) |
arclength at that end |
For a single run outside a network, use ut_run_ports(path, prof, name).
uniTube ships no threads, barbs or sockets. Terminations are a separate library that reads this record (ADR 0005, #19).
Known issue (#34). A closed run reports an
aand abport although it has no ends.
What this version does not do
- No fillets. Junctions are correct and creased. A sharp interior corner is a stress riser and shows when printed (#21).
- No branch onto a bend. Every known filleted-join method needs the base to be a plane, a sphere, or a straight cylinder or prism.
- Thin walls at a shallow crotch are not repaired. A generous ball and the
NET-4warning are what there is. - Mid-run landings work with
"ball"and"none"only.
Cost, and previewing
A single run involves no boolean and takes 0.4–60 ms. The junctions cost a CGAL evaluation: the 4-into-1 manifold renders in about 8 s.
ut_assemble() wraps its CSG in render(), so preview pays that cost too
(1.9 s for the coaxial jacket). For a model too heavy to evaluate live, pass
opts = [["preview", "fast"]] to skip it. The fast preview of the coaxial
jacket measures the same as the wrapped one; see
developer/verification.md.
Checking a finished part
A valid, manifold solid can still have a sealed bore. To assert that a bore is
open, put a keep-out region down it; checks/ does this for three examples
with partspec. See
developer/verification.md.