Surfaces

Open sheets with no inside: how to make one (an open curve swept along a path, a loft through paths), and what to do with it: split a solid along a curved cut, thicken it into a shell, sew several into a closed solid, push the pieces apart. Typical workflows, the limits, and the errors.

A bent tube split lengthwise by a surface: the surface, a ribbon, in pink, the tube's two halves, grey and see-through, pushed apart from it

What a surface is

A Surface is a sheet, as thin as paper and with no thickness at all: faces with an edge left open, so it has no inside. A solid is closed all round and has an inside that can be printed; a surface does not. It is a tool, never a part. You use it in one of four ways:

You want Use
to cut a solid along a curve instead of a plane body.split(surface)
a thin curved shell, a panel, a gutter surface.thicken(2.mm)
a closed solid whose sides you made one by one sew([sides…])
the space between surfaces, solids and planes, filled enclosed([…], at: point)
the pieces of a cut apart, an exploded view (the pieces drawn a little apart, to show how they fit) pieces.push(from: surface, by: 3.mm)

If a flat cut will do, cut with a plane: body.split(plane) is simpler (see the guide on planes). If the shape is a closed solid you can sweep, loft, extrude or revolve, make it that way (see the guide on sweeps): surfaces are for what those cannot do.

Making one

Sweep an open curve. A Path is a curve; a sweep moves a shape along a route. A Path swept along another path gives a surface, the same way a profile swept gives a solid: the curve is drawn on XY around the origin and placed across the route, its +Y towards world +Z, its x to the right seen from further along the route looking back (a route along +X takes x to +Y, one along +Y takes x to -X). up:, guide:, twist: and frenet: work as for a profile.

// a ribbon 16 mm wide along a bent route, made 1 mm thick

immutable route = path([vec(0, 0, 0), vec(40, 0, 0),
    vec(40, 40, 0)], bend: 15.mm);

@part("Sky Pop")
Solid strap() => path([vec(-8.mm, 0), vec(8.mm, 0)])
    .sweep(route)
    .thicken(1.mm);

A ribbon 16 mm wide along a bent route, made 1 mm thick

Loft through paths. A loft stretches a skin across several curves or outlines placed one after the other. loft([path1, path2, …]) through paths instead of profiles gives the smooth sheet through them, in order; ruled: true joins them with flat strips. Every element must be a path: a profile among them is an error. Two closed paths give a tube open at both ends.

The curve can come from anywhere a path comes from: path, spline, helix, a sketch's s.path, a solid's edges body.path(…), two views path(top, front) (all in the guide on sweeps).

Splitting a solid along a surface

body.split(surface) gives the pieces the surface cuts the solid into, sorted as split(plane) sorts them (along X, then Y, then Z), so .bottom, .top, .largest, [0] pick one, and @parts makes a part of each. The surface must reach across the solid, past its edges on every side. push(from: surface, by:) moves each piece away from the surface, across it where it is nearest: an exploded view, or two halves laid apart to print.

A wavy cut through Carlo: a curve drawn across X, swept along Y at mid-height:

import std;

// the wave runs along -Y, so the curve's x lands on +X;
// it reaches past Carlo on every side
Surface wave() => spline([vec(-12, 0), vec(0, 3),
        vec(12, -3), vec(24, 3), vec(36, 0)])
    .sweep(path([vec(0, 20, 7.5), vec(0, -15, 7.5)]));

@parts(varied: true)
Solid[] halves() => carlo()
    .split(wave())
    .push(from: wave(), by: 4.mm);

Carlo split by a wavy surface into a top and a bottom, pushed apart

A bent tube cut lengthwise is the same idea with the route reused: a straight line across the tube's profile, as wide as the tube, swept along the tube's own path, runs through its middle all the way. The two halves print flat, each on its cut face:

// a bent tube cut lengthwise into two halves

immutable route = path([vec(0, 0, 0), vec(40, 0, 0),
    vec(40, 40, 0)], bend: 15.mm);

Solid tube() => (circle(d: 16.mm) - circle(d: 12.mm))
    .sweep(route);

// a line across the profile, wider than the tube,
// swept along the same route: it runs through the middle
Surface ribbon() => path([vec(-10, 0), vec(10, 0)])
    .sweep(route);

// (grey and see-through here, so that the ribbon shows)
@parts("Gray Pop", alpha: 0.35)
Solid[] halves() => tube()
    .split(ribbon())
    .push(from: ribbon(), by: 4.mm);

// the ribbon itself, shown between the halves: a sheet has
// no thickness to draw, so a hair-thin solid made from it
@part("Pink Pop", export: false)
Solid sheet() => ribbon().thicken(0.05.mm);

A bent tube split lengthwise by a ribbon swept along its path: the ribbon in pink, the two halves, grey and see-through, pushed apart from it

Thickening

surface.thicken(t) makes a solid t thick from the sheet, on the side its normals point (a face's normal is the direction it looks towards, straight out of it); a negative t goes the other side. Which side that is depends on the direction of the curves, so check it: look at the picture, or at the solid's bbox, and change the sign if it went the wrong way.

// two sheets made solid: a saddle lofted through three
// curves, and a half circle swept along a line

Path edge(Length y, Length rise) => spline([
    vec(0, y, 0), vec(30, y, rise), vec(60, y, 0)]);

@part("Sky Pop")
Solid saddle() => loft([edge(0.mm, 12.mm),
        edge(20.mm, 2.mm), edge(40.mm, 12.mm)])
    .thicken(2.mm);

// the lower half of a circle, drawn in a sketch
// (an arc goes counter-clockwise, start to end)
Path halfCircle()
{
    auto s = sketch();
    auto c = s.point(0, 0, fixed: true);
    s.arc(c, s.point(-10, 0, fixed: true),
        s.point(10, 0, fixed: true));
    return s.path;
}

@part("Pink Pop")
Solid gutter() => halfCircle()
    .sweep(path([vec(0, 0, 0), vec(60, 0, 0)]))
    .thicken(1.5.mm)
    .move(y: 70.mm, z: 12.mm);

A saddle lofted through three curves and a gutter swept from a half circle, both thickened

A thickened sheet is an ordinary solid: fillet it, cut holes in it, add it to other parts.

Sewing

sew([…]) joins surfaces that close round a space into one solid, as the panels of a ball sewn together. Flat ends can be given as profiles placed where they go (rect(…), circle(…).move(z: …)). The pieces must meet edge to edge all round; where they leave a gap the error says how many edges stay open.

Often the same solid is easier to make by splitting: a box cut by its top surface. Both are below, with the same volume:

// one wavy-topped block made twice: sewn from its six
// sides, and cut from a box by the top surface

Path wave(Length y) => spline([vec(0, y, 10),
    vec(20, y, 16), vec(40, y, 8), vec(60, y, 12)]);

Path floor(Length y) =>
    path([vec(0, y, 0), vec(60, y, 0)]);

Path post(Length x, Length y, Length h) =>
    path([vec(x, y, 0), vec(x, y, h)]);

Surface top() => loft([wave(0.mm), wave(40.mm)]);

@part("Sky Pop")
Solid sewn() => sew([
    top(),
    loft([floor(0.mm), wave(0.mm)]),
    loft([floor(40.mm), wave(40.mm)]),
    loft([post(0.mm, 0.mm, 10.mm),
        post(0.mm, 40.mm, 10.mm)]),
    loft([post(60.mm, 0.mm, 12.mm),
        post(60.mm, 40.mm, 12.mm)]),
    rect(vec(0, 0), w: 60.mm, h: 40.mm)]);

@part("Pink Pop")
Solid cut() => box(60.mm, 40.mm, 20.mm)
    .move(x: 30.mm, y: 20.mm)
    .split(top())
    .bottom
    .move(x: 80.mm);

The same wavy-topped block, sewn from six sides and cut from a box

Sew when you have every side anyway (they were lofted between curves you were given); split when one surface is the only special thing about the shape.

The space between

enclosed([…]) fills the space that surfaces, solids and planes close off between them, as water poured into a mould fills what the walls leave. Each region walled in on every side becomes a solid. Give at: a point to take the one holding it, or a list of points to take several, joined; without at: you get all of them, sorted as pieces, to pick from. Planes count as walls as large as needed.

Below, a tube is cut by the floor plane and by a wavy sheet. The part between them is one volume, and its wavy top is the sheet's shape:

// a tube between a floor and a wavy sheet: the space
// they close off, filled; the lowest piece beside it

immutable tube = cylinder(d: 30.mm, h: 40.mm);

immutable wave = spline([vec(-25.mm, 0.mm),
        vec(-10.mm, 4.mm), vec(5.mm, -4.mm),
        vec(25.mm, 3.mm)])
    .sweep(path([vec(0.mm, -25.mm, 25.mm),
        vec(0.mm, 25.mm, 25.mm)]));

immutable floor = plane(origin: vec(0.mm, 0.mm, 8.mm),
    normal: Z);

@part("Sky Pop")
Solid middle() => enclosed([tube, wave, floor],
    at: vec(0.mm, 0.mm, 15.mm));

@part("Pink Pop")
Solid bottom() => enclosed([tube, wave, floor],
        at: vec(0.mm, 0.mm, 4.mm))
    .move(x: 40.mm);

The middle of a tube between a plane and a wavy sheet, and its bottom

The shapes must cross one another: a sheet that stops short of the tube's wall leaves the space open on that side, and nothing is closed off there. Make the sheets generously larger than what they cut, as for a split.

Filling a loop

fill(rim) makes a smooth face that closes a loop: a closed path, which may rise and fall. through: gives points it must pass through too, to make it a dome. The result is a surface, so it is thickened, sewn or used to split like the others.

import std;

// a closed rim that rises and falls, filled with a
// smooth face, then made into a dome through a point
Path rim()
{
    Vec3[] pts;
    foreach (i; 0 .. 24)
    {
        immutable t = i * 360.deg / 24;
        pts ~= vec(20.mm * cos(t), 14.mm * sin(t),
            4.mm * sin(2 * t));
    }
    return spline(pts, closed: true);
}

@part("Sky Pop")
Solid cap() => fill(rim()).thicken(1.mm);

@part("Pink Pop")
Solid dome() => fill(rim(), through: [vec(0.mm, 0.mm, 10.mm)])
    .thicken(1.mm)
    .move(x: 50.mm);

A wavy rim filled, and the same rim filled as a dome

On a solid, body.fill(edges, tangent: faces) closes a loop of its own edges: the rim of an opening, of an irregular cavity. Where the rim lies on one of the faces given as tangent:, the new face carries on smoothly from it; elsewhere it only meets it. Thickened, it makes a lid that follows the opening exactly.

import std;

// a block with a wavy top, and an opening shaped like
// Carlo cut down into it
immutable wave = spline([vec(-25.mm, 0.mm), vec(-10.mm, 3.mm),
        vec(5.mm, -3.mm), vec(25.mm, 2.mm)])
    .sweep(path([vec(0.mm, -20.mm, 12.mm),
        vec(0.mm, 20.mm, 12.mm)]));

immutable block = box(40.mm, 30.mm, 20.mm).split(wave)[0];

immutable pocket = carlo().projection.scale(0.5)
    .centered.extrude(30.mm).move(z: 6.mm);

immutable body = block - pocket;

@part("Sky Pop")
Solid part() => body;

// the opening closed by a face that carries on the
// wave, lifted to show it
@part("Pink Pop")
Solid cap() => body
    .fill(edges.between(faces.from(pocket), faces.farthest(Z)),
        tangent: faces.farthest(Z))
    .thicken(0.6.mm)
    .move(z: 8.mm);

An opening in a wavy top, and the face that closes it

Workflows

  • A curved cut, to split a part in two along a curve (a lid that follows a wave, two halves that interlock): draw the curve, sweep it along a straight route longer than the part, split, take the pieces as @parts.
  • A part cut lengthwise (a tube, a handle, a duct, to print in halves): sweep a line across the profile along the part's own route, split, push apart.
  • A shell from a sheet (a curved panel, a cover, a gutter, a visor): loft through curves or sweep a curve, then thicken.
  • A solid from given edges (a part whose outlines on four sides are known): loft each side between its edges, close the ends with profiles, sew.

The parameters

Call Parameter What it does Default
curve.sweep(path, …) as a profile's sweep the curve across the route as little roll as it can
loft(paths) ruled: flat strips between the paths false, smooth
body.split(surface) the surface the pieces, sorted along X, Y, Z —
pieces.push(from: surface, by:) by: how far each piece moves away from the surface —
surface.thicken(t) t the thickness; negative: the other side —
sew(list) surfaces and flat profiles joined into one closed solid —
enclosed(list, at:) at: a point (or a list): the volume holding it (several: joined) none: every volume
fill(rim, through:) through: points the face passes through too none
body.fill(edges, tangent:) tangent: faces the new face carries on from smoothly none: it only meets them

Limits

  • A surface is not a part: a @part returning one is an error. Thicken it, sew it, or split a solid with it.
  • Surfaces do not combine with + - &, and paths and profiles do not mix in one loft.
  • A split needs a surface that reaches across the solid on every side: make it generously larger than the part. One that misses the solid, or ends inside it, is an error (it must reach across it, from one side out to the other).
  • A surface lofted through closed paths whose sharp corners meet smooth curves (a square to a circle) is thickened from its paths: each offset in its own plane, outwards for a positive thickness, so the wall is that thick across the paths' planes, a little thinner where it leans.
  • Only one surface per split: to cut with two, split, then split the pieces.
  • In the window, Split… cuts with a plane only: a surface split is written in the code.

Common errors

Message What to do
return value of 'x' must be a Solid, but this is a Surface, which has no inside: thicken it, sew surfaces into a solid, or split a solid with it a part cannot be a sheet
cannot combine a Surface with '+': split a solid with it (s.split(surface)), or thicken it into a solid first use split or thicken, not + - &
sew failed: the surfaces do not close: 4 edges are open on one side a side is missing, or two sides do not meet exactly: build neighbouring sides from the same paths
enclosed failed: the shapes close off no volume: they must wall in a region on all sides a sheet stops short, or a side is missing: make the sheets larger than what they cut, or add a plane
no volume the shapes close off holds (x, y, z) mm the point is outside every region: list them with enclosed([…]) and look at where they are
thicken by 0 leaves no solid give a thickness
could not thicken the surface cleanly where its sharp edges meet smooth ones… a sheet other than a loft through closed paths, with sharp and smooth edges meeting: round its corners, or build the solid and shell it
a surface through paths takes paths only, not Profile loft profiles (a solid) or paths (a surface), not both
loft needs an array of at least 2 profiles a loft goes through two paths at least

In the application

Surfaces are made and used in the code; there is no menu entry for them. Inspect (Ctrl+I, Cmd+I on a Mac; or Ctrl+click a line of code) shows in orange what a line makes, so click the line that makes the surface to see where it lies before splitting with it. The right-click menu's Split… cuts a part with a plane (the XY, XZ, YZ planes or the clicked face's): for a curved cut, write split(surface) in the code.