Any composition of named parts comes apart along its axes, staged and labelled, by id. More...
| Import Statement: | import Clayground.Lab |
The subject contract of a lab: a Node whose ExplodePart children are the rows of a part table (ids, roles, teaching order, offsets, anchors), and the one place every lesson mechanism resolves a part's name - partAt for a mark, the finger, the camera and a card. Nothing in this file knows what the assembly is, and that is the point: a transistor, a valve or a gearbox is a file of parts, and the mechanism - the glide, the stages, the focus dimming, the assembly lines, the labels - is here.
Like the classic engineering drawing: every part travels along its own offset, along one axis or several, and a dashed assembly line runs from where it sat to where it is. Parts declare a stage, so the shell comes off at spread 1 and what is inside comes apart at spread 2; a part may contain parts, a sub-assembly that leaves as a whole and then opens on its own.
Goal and interpolant are two properties: spread and focus are what a flow step sets and an expect asserts; spreadNow and focusNow are what the frame draws. Lab.runFlow() steps sim time without an event loop, so an interpolant can never be asserted headless - only the goal can. Where a camera has to hold the whole explosion, partAt with a second argument answers where a part is GOING.
The parts are moved imperatively, from one handler, rather than by each part binding its own position to the view: a part authored with a plain position is readable next to the models it has to match silhouettes with; the same part written as an offset arithmetic binding is not.
ExplodedView3D {
id: assembly
spread: 0 // goal: 0 assembled .. stages exploded
focus: "" // goal: a part id, or nothing
labelled: "all" // which parts carry a label
labels: ({ "case": LabLang.t("anatomy.case") })
ExplodePart { row: Anatomy.partById("case"); position: ...; Model { ... } }
ExplodePart { row: Anatomy.partById("die"); position: ...
ExplodePart { row: Anatomy.partById("die.base"); ... } // nested
}
}
MarkLayer { marks: assembly.marks; view: view3d; camera: view3d.camera }
See also ExplodePart, MarkLayer, and CameraDirector.
animating : bool |
True while anything is still moving toward its goal.
assemblyLines : bool |
Draw the dashed line from each moved part's assembled place to where it is now.
dashGap : real |
The gap between two dashes.
dashLength : real |
A dash of an assembly line, in the assembly's units.
dimOpacity : real |
How far an unfocused part fades.
focus : string |
The goal: the id of the part to look at. Every part that is not it, not above it and not inside it fades to dimOpacity. Empty means nothing is focused and all parts are drawn fully.
focusNow : real |
The interpolant of focus: how far the dimming has arrived, 0 (nothing dimmed) to 1 (the unfocused parts sit at dimOpacity). Moving the focus from one part straight to another is a cut, not a crossfade.
glideMs : int |
labelled : var |
Which parts carry a label: a list of ids in the order the marks are listed, or "all" for every explained part in teaching order. Ids the subject does not have are skipped.
labels : var |
Part id to display text, the lesson's language. Data, never looked up here: the lab owns its strings. A part without an entry is labelled with its id.
lineColor : color |
The ink of the assembly lines.
lineWidth : real |
The width of an assembly line, in world units.
lines : var |
One {id, from, to} per part that has left its place, plain triples in this Node's own frame: the assembly lines as numbers, for a check that never needs a picture.
marks : var |
otherAnimating : bool |
For a subject that adds a glide of its own (an x-ray): bind this and animating covers it too.
partIds : var |
Every part's id, nested ones included, in document order.
parts : var |
The ExplodePart objects themselves, in the same order.
spread : real |
The goal: 0 assembled, 1 the first stage fully out, 2 the second, up to stages. Fractions are part-way through a stage.
stages : int |
The largest stage any part declares; spread runs 0 to this.
table : var |
The part table as the parts declare it: one {id, parent, role, order, stage, offset, anchor} per part, plain numbers, in the same order as partIds.
unit : real |
Scales every part's offset - how far apart "fully exploded" is.
idsInOrder() |
The ids a lesson walks: every part with order > 0, ascending.
vector3d partAt(string id, real atSpread) |
The part's anchor in scene coordinates as it is NOW - assembled pose plus the current displacement of it and of every sub-assembly it sits in - for a fingertip, a ring or a card. With atSpread, where the anchor WILL be at that spread: the goal, for a camera that has to hold the whole explosion before it has happened.
An unknown id answers Qt.vector3d(NaN, NaN, NaN) rather than throwing, because a lesson naming a part a subject does not have should show up as a mark that cannot be placed, not as a broken flow.
ExplodePart partOf(string id) |
The part with this id, or null.
void refresh() |
Collect the parts again - after a part was added or removed at run time. Parts declared in the file are collected on completion, so a static subject never calls this.