tscad GitHub

Refs: operate on named solids

Nesting is how tscad combines solids: <difference> wraps the body and its tools, and <fillet> wraps what it rounds. That works until the solid you want to change lives inside a component someone else wrote. Refs let you reach in from outside instead: give solids (and component calls) a name, then point an operation at them with a selector.

import { edges } from "tscad"
import { Bolt } from "tscad/parts"

export default function Bracket() {
  return (
    <part name="bracket">
      <box name="plate" size={[40, 20, 4]} />
      <Bolt name="bolt" thread="M3" length={12} z={4} />
      <subtract from=".plate" tool=".bolt > .shank" />
      <fillet target=".plate" radius={1} edges={edges.max("z")} />
    </part>
  )
}

The plate loses a hole where the bolt's shank passes through it, the shank is used up (it is no longer in the output), and the plate keeps its name, so the fillet that follows still finds it. Nesting keeps working as before; refs are an extra way to say the same thing.

Names

Selectors

SelectorPicks
.plateevery solid named plate
.bolt > .shanka shank whose nearest named ancestor is bolt
.bolt .shanka shank anywhere inside bolt
.a, .bboth

"Ancestor" means the nearest named element or component call above it. Unnamed wrappers (<group>, <translate>, a component without a name) don't count, so .bolt > .shank still works when the bolt's shank sits inside a placement group. Names are written like CSS classes, with a leading dot; "plate" without the dot is an error that says so.

Operations

ElementDoes
<subtract from=".a" tool=".b" />cuts the tool(s) from from. <difference from tool> is the same.
<union of=".a, .b" />unites them; the first one keeps its place and name
<intersect of=".a, .b" />keeps what they share (<intersection of> is the same)
<fillet target=".a" radius edges />rounds edges of one solid
<chamfer target=".a" distance edges />bevels edges of one solid
<shell target=".a" thickness open />hollows one solid
<draft target=".a" angle />drafts faces of one solid

These are the same elements you already nest; giving them from/tool, of or target instead of children switches them to refs. Everything else (radius, edges, open...) means what it always did, and selectors such as edges.max("z") are measured on the target where it sits in the tree, exactly as if the feature wrapped it there.

What happens to the solids:

Ghosts: tools that are only tools

A cutter is a solid you never want in the output on its own. Mark it ghost and it is left out unless a ref op uses it up; if nothing does, it is dropped with a warning (not an error):

<part name="plate">
  <box name="plate" size={[40, 20, 4]} />
  <cylinder name="hole" ghost r={2} h={10} z={-3} />
  <subtract from=".plate" tool=".hole" />
</part>

Scope

Refs resolve after the solids around them are built, in the order the ops are written, so an op may refer to a solid written after it.

Patterns repeat one solid in several places, so a name inside a <linearPattern> matches it several times and ref ops refuse it. Name the pattern itself, or do the op inside the patterned component.

Errors

Every error points at the line of the op that failed:

bracket.tsx:9:7: <subtract> from=".plat" matches nothing in part "bracket"
  hint: did you mean ".plate"? Named here: .bolt, .bolt > .shank, .bolt > .head, .plate
<fillet> target=".shank" matches 2 solids: .left > .shank, .right > .shank
  hint: qualify it with the parent's name, e.g. .bolt > .shank

For component authors

Why

For people this is about composition: you can drill through, round or hollow a solid that came from a library without forking the component. For AI agents writing models through the MCP server it is also shorter: <subtract from=".plate" tool=".bolt > .shank" /> is one line, where the nested form means restructuring the tree around the plate.