3D bins and weight limits¶
Before CargoForge-C can compute a ship's centre of gravity or check IMO stability
criteria, every cargo item needs a position in 3D space. place_cargo_3d in
src/placement_3d.c does that job: it divides the ship into rectangular zones called
bins, then assigns each piece of cargo to a position inside one of them while
tracking how much weight each bin is already carrying.
The mental model 🧠¶
A hold is not a flat floor — it is a room, and a box set down in it claims a three-dimensional chunk of space and leaves oddly-shaped gaps around itself. CargoForge tracks those gaps as a list of free spaces, so the next box can be slotted into a leftover pocket instead of only ever stacking on top. That bookkeeping is what lets it use the volume of a hold, not just its floor.
Weight is what makes this harder than pure geometry. A spot can be empty and still forbidden — because the bin beneath it is already at its weight cap, or because piling more there would lift the centre of gravity too high. So "does it fit?" is really three questions at once: is there room, can the structure bear it, and does it keep the ship stable? Only a yes to all three places the box, which is why place_cargo_3d carries a running weight for every bin, not just a map of empty space.
What this actually means (plain English)¶
No jargon — here's what the ideas in this lesson actually mean, and why they matter.
- Bin = "a named rectangular zone carved out of the ship's hull" —
place_cargo_3dcreates three of them (ForwardHold, AftHold, Deck), each with its own weight ceiling and a list of free spaces still available for cargo. max_weight/current_weight= "a hard cap on how many tonnes a zone can carry, and a running tally of what's already in it" —find_best_fit_3dcheckscurrent_weight + cargo->weight > max_weightfirst; if that fails the whole bin is skipped without even looking at geometry.pos_zand the z-coordinate = "how high above the keel the cargo sits, which directly controls whether the ship is stable" — holds start at z = −8 m (deep, low centre of gravity, good for stability) while deck cargo sits at z = 0 m (high, raises KG, can reduce GM and make the ship more tender).- Six orientations = "trying all six ways you can rotate a box so its three dimensions line up with the ship's three axes" — a tall crate that won't fit standing up might still slot in sideways;
find_best_fit_3dtries all six permutations and keeps the tightest fit. - Best-fit strategy = "preferring the smallest free space that still holds the item" — by not wasting large spaces on small items, the algorithm keeps big gaps available for the heavy cargo that comes next (FFD sort from Lesson 31 feeds the largest items first).
- Sentinel value
pos_x = pos_y = pos_z = -1.0f= "a flag that says this item was never placed" —perform_analysisinanalysis.cskips any cargo wherepos_x < 0, so an unplaced heavy item silently disappears from the KG and trim calculation.
Why it matters: if a heavy item cannot be placed, the stability numbers perform_analysis returns will be wrong — and the only warning is a single line on stderr. Getting the bin geometry and weight limits right is therefore a prerequisite for trusting any GM or trim result the program produces.
What a bin is¶
A bin is a named, axis-aligned rectangular box that represents a usable volume on the
ship. In CargoForge-C the Bin3D struct (declared in placement_3d.h) carries
everything the placer needs to know about that zone:
| Field | Meaning |
|---|---|
name |
Human-readable label: "ForwardHold", "AftHold", "Deck" |
x, y, z |
Position of the bin's origin (metres from the hull origin) |
width, depth, height |
Extent along X, Y, Z (metres) |
max_weight |
Hard weight ceiling for this bin (kg) |
current_weight |
Running total of placed cargo weight (kg) |
spaces[] |
Array of Space3D free rectangles available for new cargo |
space_count |
How many entries in spaces[] are valid |
A Space3D is a sub-region inside a bin that is still available. Every bin starts with
one space that fills it completely. As cargo is placed, that space is carved up into
smaller free pieces — the guillotine algorithm described in Lesson 33.
The three bins CargoForge-C creates¶
place_cargo_3d hard-codes three bins from the ship's own dimensions. From
src/placement_3d.c (lines 163–218):
// Forward hold (30% of length)
bins[0] = (Bin3D){
.name = "ForwardHold",
.x = 0.0f, .y = 0.0f, .z = -8.0f, // Below waterline
.width = ship->length * 0.3f,
.depth = ship->width * 0.8f, // Leave space for side tanks
.height = 8.0f, // Typical hold height
.max_weight = ship->max_weight * 0.3f,
.current_weight = 0.0f,
.space_count = 1
};
// Aft hold (30% of length)
bins[1] = (Bin3D){
.name = "AftHold",
.x = ship->length * 0.7f, .y = 0.0f, .z = -8.0f,
.width = ship->length * 0.3f,
.depth = ship->width * 0.8f,
.height = 8.0f,
.max_weight = ship->max_weight * 0.3f,
.current_weight = 0.0f,
.space_count = 1
};
// Deck (full length, lower weight capacity)
bins[2] = (Bin3D){
.name = "Deck",
.x = 0.0f, .y = 0.0f, .z = 0.0f, // At waterline
.width = ship->length,
.depth = ship->width,
.height = 4.0f, // Lower stacking on deck
.max_weight = ship->max_weight * 0.4f,
.current_weight = 0.0f,
.space_count = 1
};
Three design choices are worth understanding:
Proportional sizing¶
Each bin's dimensions are a fixed fraction of ship->length and ship->width.
If you load a 200 m × 30 m ship, the ForwardHold will be 60 m × 24 m. The two
holds each hold 30 % of max_weight; the Deck holds 40 %. Together that is
exactly 100 % — so if every bin filled to its limit, the ship would reach its
maximum permitted cargo weight.
The z-coordinate and what it means¶
The ship's coordinate origin sits at the hull — the lowest point of the keel is
near z = −8 m (using the box-hull's 8 m hold height). pos_z on a placed cargo
item therefore represents height above (or depth below) the keel datum:
| Bin | z |
Interpretation |
|---|---|---|
| ForwardHold | −8.0 m | Holds are below waterline; z rises toward 0 as cargo stacks up |
| AftHold | −8.0 m | Same datum |
| Deck | 0.0 m | Deck cargo sits at (or above) the waterline |
This matters directly for stability: the analysis module computes KG (vertical centre of gravity) as:
Cargo deep in the holds (large negative z) pulls KG down; deck cargo (z = 0 or positive) pushes KG up. Lower KG → larger GM → safer ship. The bin geometry therefore shapes the stability outcome directly.
Width = 80 % for the holds¶
Both holds use ship->width * 0.8 rather than the full beam. The remaining 20 %
is notionally reserved for side tanks and structural frames — a simplification that
keeps the geometry from assigning cargo to unreachable corners.
The placement loop¶
After the bins are built, place_cargo_3d processes every cargo item in the order
determined by the FFD sort (largest volume first — covered in Lesson 31). From
src/placement_3d.c (lines 220–256):
for (int i = 0; i < ship->cargo_count; i++) {
Cargo *c = &ship->cargo[i];
int best_bin, best_space, best_orientation;
if (find_best_fit_3d(ship, bins, bin_count, c,
&best_bin, &best_space, &best_orientation)) {
Bin3D *bin = &bins[best_bin];
Space3D *space = &bin->spaces[best_space];
// Set cargo position
c->pos_x = space->x;
c->pos_y = space->y;
c->pos_z = space->z;
// Update bin weight
bin->current_weight += c->weight;
// Split the space
split_space_3d(bin, best_space, c, best_orientation);
placed_count++;
} else {
fprintf(stderr, "Warning: Could not place cargo %s ...\n", c->id);
// Mark as unplaced
c->pos_x = c->pos_y = c->pos_z = -1.0f;
}
}
Two outcomes are possible for every item:
- Placed —
find_best_fit_3dfound a viable space. The item'spos_x/y/zare set to the corner of that space, the bin'scurrent_weightis incremented, and the space is split into free remainders. - Unplaced — no space passed all the checks. The sentinel value
pos_x = pos_y = pos_z = -1.0fis written. Every downstream calculation that sums cargo moments checkspos_x >= 0before including an item, so unplaced cargo is silently excluded from the stability analysis.
Silent exclusion from analysis
If a heavy item cannot be placed, it disappears from the stability calculation.
The printed warning on stderr is the only signal. Always check the placement
summary lines for n/N items placed before trusting an analysis result.
How find_best_fit_3d enforces weight limits¶
Weight is the very first check inside find_best_fit_3d. From
src/placement_3d.c (lines 61–67):
for (int b = 0; b < bin_count; b++) {
Bin3D *bin = &bins[b];
// Check weight constraint
if (bin->current_weight + cargo->weight > bin->max_weight) {
continue;
}
/* ... rest of space/orientation search ... */
}
If adding this item would push the bin past its max_weight, the entire bin is
skipped without even looking at individual spaces. This is O(1) per bin and
happens before the more expensive constraint checks.
Only after passing the weight gate does the code walk every free space and try all six axis-aligned orientations. The chosen space is the one with the smallest volume that still fits the item — a best-fit strategy that leaves the largest spaces available for future items:
float best_fit_volume = 1e9f; // Start with "impossibly large"
for (int o = 0; o < 6; o++) {
if (fits_in_space(space, cargo, o)) {
float vol = space_volume(space);
if (vol < best_fit_volume) { // Prefer smaller spaces
best_fit_volume = vol;
*best_bin = b;
*best_space = s;
*best_orientation = o;
}
}
}
fits_in_space is a straightforward dimension check — width, depth, and height of
the cargo (in the chosen orientation) must each be ≤ the corresponding space
dimension. space_volume is simply width × depth × height.
When weight forces a different bin¶
Suppose ForwardHold has 595 t of its 600 t capacity used and a 10 t item arrives.
The weight check fails for ForwardHold. The code moves on to AftHold, then Deck.
If neither can take the item either, find_best_fit_3d returns 0 (failure) and
the item is marked unplaced.
Six orientations — why they matter¶
A container that is 6 m × 2.5 m × 2.6 m can be loaded in any of six axis-aligned
rotations. From src/placement_3d.c:
switch (orientation) {
case 0: *w = dims[0]; *d = dims[1]; *h = dims[2]; break; // XYZ (as-declared)
case 1: *w = dims[0]; *d = dims[2]; *h = dims[1]; break; // XZY
case 2: *w = dims[1]; *d = dims[0]; *h = dims[2]; break; // YXZ
case 3: *w = dims[1]; *d = dims[2]; *h = dims[0]; break; // YZX
case 4: *w = dims[2]; *d = dims[0]; *h = dims[1]; break; // ZXY
case 5: *w = dims[2]; *d = dims[1]; *h = dims[0]; break; // ZYX
}
All six permutations of the three dimensions are tried for every candidate space. This means a tall, narrow crate that won't stand upright in a short space might still fit if rotated onto its side. The best-fitting orientation is kept.
Real-world constraint not yet modelled
CargoForge-C does not yet enforce "this-way-up" markings. Every cargo item is
treated as freely rotatable. Fragile or liquid cargo in practice has orientation
constraints that a future version could add to the Cargo struct.
Placement summary output¶
After the loop, place_cargo_3d prints a summary to stderr. Each bin reports its
used weight versus its limit:
3D Placement complete: 8/10 items placed
ForwardHold: 580000.0 / 600000.0 kg (96.7% capacity)
AftHold: 420000.0 / 600000.0 kg (70.0% capacity)
Deck: 200000.0 / 800000.0 kg (25.0% capacity)
This is the first place to look when an item fails to place — a bin at 96 % with a large remaining item is a weight-limit rejection, not a geometry failure.
How position feeds into analysis¶
Once place_cargo_3d returns, every item that was placed has valid pos_x,
pos_y, pos_z coordinates. perform_analysis in analysis.c then does a
single pass over ship->cargo, skipping items where pos_x < 0:
pos_z + dimensions[2]/2is the vertical centroid of the item — used to compute the cargo's contribution to KG.pos_xrelative toL/2determines the item's longitudinal moment — used for trim and for the shear-force/bending-moment distribution.pos_yrelative toB/2determines the transverse moment — used for heel.
The bin structure itself is discarded after place_cargo_3d returns; only the
coordinates written onto each Cargo struct persist into analysis.
Recap¶
- CargoForge-C partitions the ship into three hard-coded
Bin3Dzones: ForwardHold (30 % length, z = −8 m), AftHold (30 % length, z = −8 m), and Deck (full length, z = 0 m). - Each bin carries a weight ceiling (
max_weight): 30 % / 30 % / 40 % of the ship'smax_weightrespectively. find_best_fit_3drejects a bin immediately if adding the item would exceedbin->current_weight + cargo->weight > bin->max_weight.- Items that pass the weight gate are tried in all six axis-aligned orientations; the tightest-fitting space (smallest space volume that still accommodates the item) wins.
- Items that cannot be placed receive the sentinel
pos_x = pos_y = pos_z = -1.0fand are excluded from every downstream stability calculation. - The
pos_x/y/zcoordinates written by the placer feed directly into KG, trim, heel, and longitudinal-strength calculations inanalysis.c.
Check yourself¶
Why does a hold need to track both free 3D space and a running weight total, instead of just one?
A spot can be geometrically empty and still forbidden — if the bin is already at its weight cap, or if adding weight there would push stability past a safe limit. "Does it fit?" is really three separate questions (room, structural capacity, stability), not one.
What does split_space_3d do right after a box is placed?
It cuts the free space around the newly placed box into up to three new rectangular free spaces — a right-remainder, a back-remainder, and a top-remainder — so later items still have somewhere to land.