Assembly YAML
Introduction
Assembly YAML is an approach to define assemblies using a simple YAML file. This file format was introduced in PartCAD for the first time.
Syntax
Each Assembly YAML (ASSY) file is a YAML file consisting of a tree of nodes. Each node is either a reference to an external part, a reference to an external assembly, or a container for such references.
Containers
A container node is a node that holds other nodes rather than placing an object. The container nodes have the following syntax:
name: <(optional) name> description: <(optional) description> location: <(optional) OCCT Location object> # e.g. [[0,0,0], [0,0,1], 0] links: - <other node> - <other node> - <other node> - <other node>
The top-level node of an ASSY file is such a node, and it is the assembly: what it holds is held by the assembly directly, not by anything in between. So an assembly declared from a file whose root lists three parts has three nodes in it and not one, and location on that root moves all three - there is no node of its own for it to move instead. A links: list inside the file is a node like any other: it is addressable by name from a map:, and what it holds belongs to it.
What a link is called
Every link of an ASSY file has a name, and one rule gives it:
its own
name:, where it gives one;otherwise the part or the assembly it places;
otherwise its position in the
links:list it is written in, one-based, aslink#2.
Only a links: container reaches the third – a node that places something is
named after it – and it exists so that there is no such thing as a link that
cannot be named. That one name is what everything uses: what a connect:
names as the link to connect to, what a map: names as a node (joined with
/ for a path of them, head/head_half_1), what a connection’s
interferes: names, what a filter selects, and what the
PartCAD extension for VS Code
shows as the row for that link in the viewer.
It is not the same thing as the object a link places, and the viewer shows both: an assembly that places the same bolt a hundred times holds one object under a hundred link names, and it is the link name that says which of them this one is. So a position is used rather than the object’s name where there is nothing else – a name shared by a hundred rows would say nothing about any of them.
Names need not be unique, though. A file that places four legs in a loop may
call every one of them leg, and a name every leg answers to cannot pick one
of them out. So a filter may also name any link by its
position, link#3, whatever it is otherwise called – the IDE’s 2D and Draft
panels name a link that way exactly when its name is shared with another link
beside it.
links: - part: bolt # called 'bolt' - part: bolt name: upper # called 'upper' - name: head # called 'head' links: - part: head_half # called 'head_half', and 'head/head_half' from a 'map:' - links: # called 'link#4': the fourth entry of this list - part: plate # called 'plate', and 'link#4/plate' from a 'map:'
A positional name is relative to the list it is written in, so adding a link above one renames it. Name a container you intend to refer to.
Parts
The following syntax is used to create a node that places a part in the assembly:
part: <name of a part from this package or a global path "{/package}:{part}"> name: <(optional) name to use for this part in this assembly> description: <(optional) free form text, see "Description" below> location: <(optional) OCCT Location object> # e.g. [[0,0,0], [0,0,1], 0] connectPorts: # alternative to "location", used to connect by ports with: <(optional) name of the port in this part, if more than one exists> name: <the name of the target part in this assembly to connect to> to: <(optional) name of the port in the target part to connect to, if more than one exists> comment: <(optional) free form text, see "Comment" below> how: <(optional) assembly instructions, see "How" below> exploded: <(optional) the gap to show in the exploded view of this step, in mm> connect: # alternative to "location" and "connectPorts", used to connect by interfaces with: <(optional) name of the interface in this part, if more than one exists> withInstance: <(optional) name of the instance of the interface in this part, if more than one exists> withPort: <(optional) name of the port in this part to connect with> name: <the name of the target part in this assembly to connect to> to: <(optional) name of the interface in the target part to connect to, if more than one compatible one exists> toInstance: <(optional) name of the instance of the interface in the target part to connect to, if more than one exists> toPort: <(optional) name of the port in the target par to connect to> comment: <(optional) free form text, see "Comment" below> how: <(optional) assembly instructions, see "How" below> exploded: <(optional) the gap to show in the exploded view of this step, in mm> interferes: <(optional) named nodes this one also ends up sharing space with, see "Expected overlaps" below>
The name a node is given is how everything else in and around this assembly refers to it: the name of a connect/connectPorts names a node, and so does the first element of the map: section that decides which of the ports inside this assembly the assembly itself has (see Ports and interfaces of an assembly). A node with no name of its own is known by the name of the part or assembly it places, which is enough until the same one is placed twice.
One and only one method for placing the object is acceptable. Therefore the sections location, connectPorts and connect are mutually exclusive. Both connectPorts and connect are used to connect parts to each other by matching their ports. connect is universal, while connectPorts is specific to connecting with ports and without interface mating.
The with and to fields are used to specify the names of interfaces. Where with is the interface of the part that is getting added to the assembly, and to is the interface of the part that is already in the assembly. The withInstance and toInstance fields are used to specify the names of interface instances. The withPort and toPort fields are used to specify the names of ports.
The exploded field does not affect the assembly itself: it is how far apart the two parts are drawn in the exploded view of this step in the assembly instruction book (pc render -t pdf and pc render -t html). Without it, the two are spaced by half of the largest dimension of the two.
Expected overlaps
pc test checks that the parts of an assembly do not share space - except where a joint says they must. A pin snapped into its hole, a screw cutting its own thread: the joint is made by occupying the material, and a model that holds no springs holds it overlapping. Three things say so, and all three are read from the joint, never declared against a pair of parts:
the mating or the how of the connection says snapIn or selfScrew, or an interface it joins cuts its own thread (selfScrew). The overlap expected is between the two parts that provide the joined ports - followed through any map: of a sub-assembly to the part inside it that provides the port;
interferes lists the further nodes the same act of joining drives into: a screw connected to the part its head bears on cuts its thread in the ones underneath as well, and a sub-assembly fitted by one of its pins drives its other pins into the same neighbour.
interferes names nodes of the same links: list as the connection, by the name of the link, which every link has: a node inside a links: container is container/name, and a container that gave itself no name is a level of that path like any other (link#2/bracket). A node may be a part or an assembly. Naming an assembly excuses every overlap between the two, which is as precise as the declaration is: a sub-assembly with a mapped port is named rather than the part inside it that provides the port, since it is the sub-assembly that is fitted.
An assembly answers only for what it puts together. A sub-assembly declared in partcad.yaml and placed in it has a verdict of its own, which the assembly takes rather than repeats: only the overlaps between what it places are looked at there, and every assembly - declared or a links: container - is first compared by the box around everything in it.
Assemblies
The following syntax is used to create a node that places an assembly in the assembly:
assembly: <name of an assembly from this package or a global path "{/package}:{assembly}"> ... # same as for parts
Description
The description field of any node is free form text about that node: the part or the assembly it places, or - on a container node - the sub-assembly it declares.
description: | A NEMA 17 stepper on a printed bracket. links: - part: example-bracket description: The fixture everything else is mounted on.
It is used when the assembly is documented (pc render -t pdf and pc render -t html): the description of the file’s own root node is what the assembly instruction book says about the assembly as a whole, unless the package that declares the assembly gives it a desc of its own; the description of a container node is what the book says about that sub-assembly; and the description of a part or assembly node is shown with the step that adds it. Nothing else in PartCAD parses it or acts upon it.
Comment
The comment field of a connect or connectPorts section is free form text.
connect: name: example-bracket comment: | The motor is easier to align if the bracket is laid face down first.
It is supplementary context for a human, or for an LLM, that is reading the assembly. PartCAD never parses it and never acts upon it; it is shown as a note beside the step this connection makes in the assembly instruction book, and reported by pc info.
Warning
comment is not a place for assembly instructions. Everything that is required to perform the assembly must be codified in the other ASSY fields, so that the tools that consume the assembly - and not just the people reading it - have the complete picture. Anything stated only in comment is invisible to them.
How
The how field of a connect or connectPorts section describes how the object is expected to be brought into place. All of its fields are optional, and so is the section itself: an omitted field means the default below.
how: stage: <(optional) name of the group of steps performed at the same time> pushForceMax: <(optional) maximum linear force, in N, default: 5> pushDistance: <(optional) staging distance, in mm, derived from the object by default> turnDirection: <(optional) "cw" (clockwise) or "ccw" (counterclockwise), default: "cw"> turnTorqueMax: <(optional) maximum torque, in N*m, default: 0> snapIn: <(optional) whether the object is pushed past a feature that springs back behind it> selfScrew: <(optional) whether this joint cuts its own thread, default: from the interfaces> threadStep: <(optional) axial distance per full turn, in mm, default: 0.00> holdWith: <(optional) interface, or list of interfaces, to hold this object by> holdWithInstance: <(optional) instance of each interface listed in "holdWith"> holdWithForceMin: <(optional) least force to hold this object with, in N, default: 3> holdWithForceMax: <(optional) most force to hold this object with, in N, default: 7> holdWithForce: <(optional) sets both "holdWithForceMin" and "holdWithForceMax"> holdTo: <(optional) interface, or list of interfaces, to hold the target object by> holdToInstance: <(optional) instance of each interface listed in "holdTo"> holdToForceMin: <(optional) least force to hold the target object with, in N, default: 3> holdToForceMax: <(optional) most force to hold the target object with, in N, default: 7> holdToForce: <(optional) sets both "holdToForceMin" and "holdToForceMax">
The units are SI, with the exception of lengths, which are in millimetres like everywhere else in PartCAD:
Field |
Quantity |
Unit |
|---|---|---|
|
force |
|
|
length |
|
|
torque |
|
|
length |
|
|
force |
|
Pushing an object into place is a linear motion, so pushForceMax bounds a
force in newtons. Turning it is a rotation, so turnTorqueMax bounds a
torque in newton-metres.
Note
pushForceMax was called pushTorqueMax in the first draft of this
section. The old spelling is still accepted, with a warning, and means the
same thing.
threadStep is the lead: the axial distance the object advances per full turn. For a multi-start thread that is the pitch multiplied by the number of starts, not the pitch itself. When it is non-zero, the assembler manages the pushing force and the turning torque together, so that the resulting motion reproduces the given thread (or, more generally, rotation) pattern, staying within pushForceMax and turnTorqueMax. The default of 0.00 means a straight push with no turning.
A thread belongs to the interfaces rather than to any one connection, so it is normally declared there and left out here - see The thread of an interface below. Given on a connection, it overrides what the interfaces say for that one step.
pushDirection is not a field: it is deduced from the connection itself and reported alongside the rest. It is the direction, in the assembly’s coordinates, that the object travels while it is pushed into place - so the object starts at pushDistance back along it.
The interfaces are what it is deduced from. The positive Z axis of an interface
points into the object the interface belongs to, which is exactly the way a
part coming to meet it has to travel. examples/feature_interface shows this
directly: both faces of the same 3mm bracket are instances of one interface,
the outer one at z=0 unrotated and the inner one at z=3 turned
around, so the two axes point at each other through the material. Each of the
four M3 screws in that example is connected to one of those faces, and its
deduced direction points from the screw through the bracket towards the motor -
reversing, as it must, when the motor is placed on the other side.
pushDistance is how far away from its final place the object starts: the distance, along pushDirection, between the staging pose and the pose where its with ports coincide with the target’s to ports. When it is omitted, it is derived from the object being connected, as 1.5 times that object’s own length along the Z axis of the interface location it is connected by - far enough for the object to clear the target before the insertion move begins. That measurement needs the object’s geometry, so it is made only when something asks for it, never while the assembly is merely being instantiated. If the geometry cannot be built at all, the distance is reported as unset rather than guessed.
stage names a group of steps that are expected to be connected at the same time rather than one after another: consecutive nodes carrying the same stage form one such group. It is a free-form label, so it can say what the group is for. A stage that is interrupted by another one and then resumes is reported, since the steps it names are then not consecutive and will not be performed together.
links: - part: example-bracket # Both screws are snugged at the same time, not one after the other. - part: socket-head-m3-screw-6mm name: screw-tl connect: name: example-bracket to: m3-thru-3 toInstance: outer-TL how: { stage: snug, turnTorqueMax: 0.2 } - part: socket-head-m3-screw-6mm name: screw-br connect: name: example-bracket to: m3-thru-3 toInstance: outer-BR how: { stage: snug, turnTorqueMax: 0.2 }
holdWith names the interface of the object that is getting added to the assembly, to be used to hold that object while it is connected. holdTo names the interface of the object that is already in the assembly, which is held the same way. Either may be a single interface name or a list of them.
- part: socket-head-m3-screw-6mm connect: name: example-bracket to: m3-thru-3 toInstance: outer-TL comment: Tighten this screw last, once the other three are started. how: turnTorqueMax: 1.2 threadStep: 0.5 holdWith: m3-screw-6mm holdTo: nema-17-motor-bracket-3 holdToInstance: outer
When holdWith is omitted, it defaults to the hold field of the connect section of the part or the assembly being added (see Holding an object). When holdTo is omitted, it defaults to the hold field of the object being connected to. When neither is set, the assembler is free to hold the object however it sees fit.
holdWithInstance and holdToInstance select which instance of the interface to hold by, for the cases where the object implements more than one instance of it. They are matched positionally against holdWith and holdTo. When omitted, the instance defaults to the holdInstance field of that same section, and, failing that, to the first instance of the interface.
holdWithForceMin and holdWithForceMax bound the force each end is held with, in newtons - enough to keep the object from moving, not so much as to damage it. holdWithForce sets both at once, and holdToForce* are the same for the object being connected to. Each bound is resolved on its own, from the first of these that gives it:
the bound itself, holdWithForceMin or holdWithForceMax;
holdWithForce, which sets both;
the holdForceMin/holdForceMax field of the connect section of the part or the assembly (see Holding an object);
the holdForce field of that same section, which sets both;
the defaults, 3 N for the minimum and 7 N for the maximum.
A minimum that ends up above its maximum is a contradiction rather than a range: it is reported, and both bounds fall back to the defaults.
The thread of an interface
An interface may declare the thread that connections made through it advance along, and whether it cuts its own:
interfaces: m3: abstract: True threadStep: <(optional) axial distance per full turn, in mm> selfScrew: <(optional) whether this interface cuts its own thread, default: false>
Both are inherited by the interfaces that inherit this one, so a thread is spelled out once, on the interface that introduces it. In examples/feature_interface the abstract m3 interface carries 0.5 and the abstract m4 carries 0.7 - the real pitches of those screws - and every opening, screw and hole pattern derived from them connects with the right thread without repeating it.
A connection inherits threadStep from the two interfaces it mates. They have
to agree on it: two different threads cannot be screwed together, and a
connection whose ends disagree is reported and fails pc test. Unless, that
is, one of them declares selfScrew - a self-tapping screw, or the plain hole
one goes into. Then the thread of the end that does have to match one is the
thread that gets cut, and failing that the one the self-tapping end brings with
it.
How a mating goes together
A pair of interfaces that always goes together the same way - an M3 screw into
an M3 tapped hole, a clip onto its rail - says so once, in a how section of
its mating, instead of on every connection:
interfaces: m3-screw: mates: m3-tapped: how: turnTorqueMax: 0.6 turnDirection: cw threadStep: 0.5 clip: mates: rail: how: snapIn: true pushForceMax: 20
It takes the fields of a connection’s how that say how the object goes in:
pushForceMax, pushDistance, turnDirection, turnTorqueMax,
threadStep, selfScrew and snapIn. stage is not one of them -
which steps are done together is the assembly’s to say - and nor are the
hold* fields, which belong to the objects (see Holding an object).
Every connection made through the pair gets them, and an ASSY file overrides
them field by field: a step whose own how gives only turnTorqueMax
keeps the mating’s thread and direction. The order, most specific first, is the
connection’s how, the mating’s how, the mating’s own selfScrew and
snapIn, the interfaces’ threadStep and selfScrew, and the defaults.
Reading the instructions back
Everything resolved from the sections above - the inherited threads, the deduced push direction, the forces each end is held with - is reported by
pc info -a <assembly>
under Connections, one entry per connected object. That is the assembly’s
own account of how it is built, for a person or for whatever performs it.
Testing the instructions
Everything above is resolved leniently: a field that is missing takes its
default, and one that contradicts itself is reported and replaced with a
default, so that an assembly always builds. pc test is what looks past that
leniency. Its connect test fails an assembly whose instructions had to be
repaired to be usable, and passes one that is sound - including one that says
nothing at all and takes every default.
What it rejects today:
a
*Minabove its corresponding*Max-holdWithForceMinaboveholdWithForceMax, orholdToForceMinaboveholdToForceMax. A minimum above a maximum is a contradiction rather than a range.two interfaces that disagree about their threadStep with neither declaring selfScrew.
pc test -a connect-instructions
Steps that can be followed
Defaults are enough for an assembly to build, and they are not enough for a
person to put it together. So an assembly that is meant to be made
(manufacturable: true) is held to more by pc test: every item after the
first of each list of links has to be connected (connect or
connectPorts), and something has to say how it goes into place - pushed,
snapped or screwed in. That is said by any of:
the connection’s own
how, with at least one ofpushForceMax,pushDistance,turnDirection,turnTorqueMax,threadStep,selfScreworsnapIn(stageand thehold*fields say when and by what, not how);the mating of the two interfaces it connects through: any field of its
how(see “How a mating goes together” above), or its ownsnapInorselfScrew, which then says it for every connection that pairs them;the interfaces themselves, with a
threadSteporselfScrew: a thread is screwed in.
The manufacturability test reports each step that says none of these; an
item placed by location: is the connectivity test’s to report. The IDE’s
Build vs Buy table shows an assembly with such steps as Incomplete, listing
them, until they are fixed.
Holding an object
The defaults for the how fields above are declared once, next to the part or the assembly itself, in partcad.yaml:
parts: example-bracket: type: step connect: # (optional) hold: <(optional) interface, or list of interfaces, to hold this part by> holdInstance: <(optional) instance of each interface listed in "hold"> holdForceMin: <(optional) least force to hold this part with, in N> holdForceMax: <(optional) most force to hold this part with, in N> holdForce: <(optional) sets both "holdForceMin" and "holdForceMax"> assemblies: motor-mount: type: assy connect: # (optional) same as for parts hold: <...>
Everything an object contributes to every connection it takes part in lives in that one connect section, rather than among the fields that describe the object itself.
Scenes
The same file format defines a scene: a placed arrangement of objects, such
as a workcell, a table with parts laid out on it, or a simulation world. A
package declares one by pointing at the file from its scenes: section rather
than its assemblies: section (see Scenes); there is no assembly object
in between, and nothing about the file itself says which it is.
scenes:
workcell:
type: assy
Everything above applies, with one exception: a scene may not use how.
An assembly is a product and says how it is put together, which is what the
assembly instruction book is generated from. A scene states only an end state,
and nothing in it was assembled, so there is nothing to say about the
assembling. Declaring how in a scene is an error, both while the scene is
being built and in the editor.
The connect and connectPorts sections themselves stay. Placing a
gripper against the fixture it holds is a statement about where things are, and
saying it with the ports the two objects declare is better than saying it with
coordinates somebody worked out by hand.
links: - part: //pub/robotics:bench name: bench - assembly: //pub/robotics:gripper name: gripper connect: name: bench with: mount to: rail # 'how' would be an error here
Part of an assembly
A sub-assembly worth looking at on its own is often not an object of its own: the head of the logo, one bay of a rack, the parts that arrive in one box. A filter names the links to keep and leaves the rest out.
A filter is a tree of link names mirroring the tree the ASSY file already is:
head: # kept, and only the children named under it
head_half_1:
bolt: # kept, with everything inside it
Three rules, and nothing else:
a link the filter names is kept, and a link it does not name is dropped;
a link named with nothing under it keeps everything under it;
a link named with at least one child under it keeps those children and drops its other ones.
A link is named the way everything else names it: its
name:, the part or the assembly it places, or its position in its links:
list. So there is no link a filter cannot name.
A filter may equally be written as a list, which is the convenient form when nothing below the names matters:
- head
- bolt
There are three ways to use one, and they do different things.
Filtering for one picture
pc render and pc export take --filter, and write nothing into the
package: the assembly is built as it always is and the filter is applied to the
result, for this one file.
pc render -a -t png --filter head.yaml -O ./ logo
pc render -a -t png --filter '{bone1: null, bone2: null}' -O ./ logo
pc export -a -t step --filter '[bone1, bone2]' -O ./ logo
The argument is a filename first: a file somebody keeps beside the package and edits. Only when there is no such file is the text read as the filter itself, as JSON or YAML – which is the convenient form for one link and for whatever a script composes. A name that is neither is refused, naming both things that went wrong, so a mistyped filename is not quietly read as a link name.
Everything kept stays where the whole assembly put it. A filter never re-resolves
a connect:, which is the only reading of “part of this assembly” that is true
of the thing on the bench. Because the result is built rather than written, a
filter here may also select inside a link that places an assembly of another
package.
The same options are available in the
PartCAD extension for VS Code:
the 2D and Draft tabs of the viewer list what the object is made of
beside the drawing, with a checkbox on every item, and what is ticked is the
filter the render is made with. On the 2D tab the list also holds each node’s
ports and interfaces, and ticking one draws that port on the projection – the
same thing --with-ports, --with-interfaces and --port ask for. All
three Design tabs share one selection, so what is ticked on the 3D view is what
the 2D and Draft tabs draw, and a box cleared on one is cleared on the others.
Filtering every time the file is written
A filter: on a render: or export: file type says the same thing
permanently, so a package can keep a picture of one sub-assembly checked in
beside the drawing of the whole of it. It is declared per file type, in either
place a file type is configured – the package’s own section, and the object’s:
render:
png:
# Every PNG this package writes is of these links only.
filter: [base]
assemblies:
widget:
type: assy
render:
svg:
# And this object's SVG is of these, whatever the package said.
filter:
tower:
upper:
step: {} # the whole of it, as ever
pc render --filter and pc export --filter override it for one run, the
way --view overrides a configured viewport. A part and a sketch have no
links, so a filter over one of them is reported rather than ignored.
Filtering into a new object
pc filter writes a declaration instead: the ASSY file of SRC with
everything the filter drops taken out of it, as DST.assy, declared beside
SRC.
pc filter head.yaml logo logo_head
pc filter '{bone1: null, bone2: null}' logo logo_bones
DST is then an object like any other: it renders, it exports, it has a bill
of materials, suppliers quote for it, and it is a file somebody can review as a
diff. If SRC is an assembly then DST is an assembly, and if SRC is a
scene then DST is a scene – the kind follows the source. An existing DST
is overwritten; its declaration is left alone when it already points at the same
file, so whatever file types were configured for it survive a re-run.
The file is filtered rather than the built assembly, which is what makes the
result an assembly somebody can build: every link kept keeps its connect:,
its how:, its parameters and its comments. (That is also why pc filter
needs an Assembly YAML file to work from. A step or urdf assembly has no
links of its own, and pc convert assembly -t assy is what turns one into a
file that does.) The one thing it cannot do is select inside a link that places
a part or another package’s assembly; it says so and keeps that link whole.
A link whose name was its position has that position written into the copy as
a name:. Filtering moves links – the second of five is the first of two once
the one above it has gone – so a position would otherwise rename it, and
anything that named it would point at nothing.
What filtering can break is exactly one thing, and it is checked: a link that
is kept may be connected to a link that was dropped. pc filter reports
every one of them, naming the line, and so does pc lint over
the file afterwards.
logo_head.assy:12:13: nothing in this file places a link called 'bolt'
Name those links in the filter too, or place the kept ones with location:.
Validation
ASSY files are checked against a JSON schema
(src/partcad_utils/schema/assy.json) that describes the
document after Jinja2 rendering: the node keys above, the shape of an OCCT
location, and the fact that location, connectPorts and connect – or
part, assembly and links – exclude one another.
One thing is checked beside the schema, because no schema can describe it: a
connect:/connectPorts: names a link of the same links: list,
written above this node, since that is where PartCAD looks for it as the
assembly is built. So does every name in a connection’s interferes:, which
may be anywhere in the file. A name that is wrong would otherwise be answered,
much later, with “Target part not found” and a part at the origin.
A file read as a scene is checked against the same schema with how
forbidden. The scene schema is derived from the one above rather than kept
beside it, so the two cannot drift apart; only the one rule differs.
Because the file on disk is a template rather than the YAML it renders to, the
checker masks every Jinja2 construct before parsing: {{ expr }} becomes a
placeholder value and {% tag %} becomes blanks of the same size. The masked
document keeps the exact line and column layout of the file, so a template
error, a YAML error and a schema violation are all reported at the character
they came from, and a loop or conditional body is checked once. Anything the
mask makes unknowable – what an expression evaluates to, which branch of an
{% if %} is taken – is left unreported rather than guessed at.
Run the checks over a package from the command line with:
pc lint
or narrow it to ASSY files only:
pc lint -f AssySchema
To check individual files instead, name them. This runs in the pc process
itself – no daemon, no package to load – so it answers even when the package
does not:
pc lint --file logo.assy --file desk.assy
Add --json for machine-readable findings (zero-based line and column
numbers), and --stdin to check content that has not been saved, which is how
an editor checks the buffer on screen:
pc lint --file logo.assy --stdin --json < draft.assy
Whether a single file is checked as an assembly or as a scene is not something
the file says, so pc lint --file works it out from the partcad.yaml
files around it: a file at least one scene declares and no assembly declares is
a scene. Say so outright with --schema:
pc lint --file workcell.assy --schema scene
The PartCAD extension for VS Code
runs exactly that while an ASSY file is open and shows the findings in the
Problems view. It answers the same question from the package contents it has
already loaded, which is the declaration itself rather than a guess at it, and
falls back to the command’s own answer for a file no loaded package mentions.
Set partcad.lint.enabled to false to turn that off.
partcad.yaml is checked the same way, by the same checker, against the
configuration schema (src/partcad_utils/schema/partcad.json) – see
Configuration. It is a Jinja2 template too, so it is masked before
parsing exactly as an ASSY file is, and it has no assembly/scene flavor:
nothing points at a package configuration, so --schema does not apply to
one.
pc lint --file partcad.yaml pc lint -f PartcadSchema # the same check over a whole package