feat(editor): add --gen-mesh-bedroll camp sleeping prop

61st procedural mesh primitive. Builds a bedroll prop:

  • horizontal closed cylinder along the Z axis sitting at
    y = R so it rests on the ground (radius defaults to
    0.16 m, length 1.4 m — adult-human-shaped)
  • optional pillow box at the +Z end (squashed cube,
    pillowSize controls extent)
  • per-segment N-sided cylinder uses the new addVertex
    helper; pillow uses addFlatBox

Set pillowSize=0 for a bare rolled mat. Watertight under
weld (verified via --info-mesh-stats: 90 manifold edges,
0 boundary, 0 non-manifold).

Pairs naturally with --gen-mesh-tent / --gen-mesh-firepit /
--gen-mesh-canopy for outdoor camp scenes — completes the
tent-side camping kit.
This commit is contained in:
Kelsi 2026-05-09 11:39:33 -07:00
parent 65b3352b9f
commit e23b3faa1c
3 changed files with 135 additions and 1 deletions

View file

@ -51,7 +51,7 @@ const char* const kArgRequired[] = {
"--gen-mesh-throne", "--gen-mesh-coffin", "--gen-mesh-bookshelf",
"--gen-mesh-tent", "--gen-mesh-firepit", "--gen-mesh-woodpile",
"--gen-mesh-canopy", "--gen-mesh-haystack", "--gen-mesh-dock",
"--gen-mesh-pergola", "--gen-mesh-chimney",
"--gen-mesh-pergola", "--gen-mesh-chimney", "--gen-mesh-bedroll",
"--gen-mesh-table", "--gen-mesh-lamppost", "--gen-mesh-bed",
"--gen-mesh-ladder", "--gen-mesh-well", "--gen-mesh-signpost",
"--gen-mesh-mailbox", "--gen-mesh-tombstone", "--gen-mesh-crate",

View file

@ -6245,6 +6245,137 @@ int handleTent(int& i, int argc, char** argv) {
return 0;
}
int handleBedroll(int& i, int argc, char** argv) {
// Camp bedroll: a horizontal closed cylinder lying along the
// Z axis at ground level (y = R), with an optional flatter
// pillow box at the +Z end. Pairs naturally with --gen-mesh-
// tent / --gen-mesh-firepit for camp set dressing. Uses the
// shared addVertex helper plus addFlatBox for the pillow.
// The 61st procedural mesh primitive.
std::string womBase = argv[++i];
float length = 1.4f;
float radius = 0.16f;
int sides = 12;
float pillowSize = 0.18f; // 0 → no pillow
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { length = std::stof(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { radius = std::stof(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { sides = std::stoi(argv[++i]); } catch (...) {}
}
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { pillowSize = std::stof(argv[++i]); } catch (...) {}
}
if (length <= 0 || radius <= 0 || sides < 6 || sides > 64 ||
pillowSize < 0 || pillowSize >= length * 0.5f) {
std::fprintf(stderr,
"gen-mesh-bedroll: dims > 0; sides 6..64; pillow < length/2\n");
return 1;
}
if (womBase.size() >= 4 &&
womBase.substr(womBase.size() - 4) == ".wom") {
womBase = womBase.substr(0, womBase.size() - 4);
}
wowee::pipeline::WoweeModel wom;
wom.name = std::filesystem::path(womBase).stem().string();
wom.version = 3;
const float pi = 3.14159265358979f;
const float halfL = length * 0.5f;
// Z-axis cylinder centered at (0, radius, 0). Same end-cap fan
// pattern used by --gen-mesh-woodpile's logs.
uint32_t back = static_cast<uint32_t>(wom.vertices.size());
for (int s = 0; s <= sides; ++s) {
float u = static_cast<float>(s) / sides;
float ang = u * 2.0f * pi;
glm::vec3 dir(std::cos(ang), std::sin(ang), 0.0f);
glm::vec3 p(radius * dir.x, radius + radius * dir.y, -halfL);
addVertex(wom, p, dir, {u, 0});
}
uint32_t front = static_cast<uint32_t>(wom.vertices.size());
for (int s = 0; s <= sides; ++s) {
float u = static_cast<float>(s) / sides;
float ang = u * 2.0f * pi;
glm::vec3 dir(std::cos(ang), std::sin(ang), 0.0f);
glm::vec3 p(radius * dir.x, radius + radius * dir.y, +halfL);
addVertex(wom, p, dir, {u, 1});
}
for (int s = 0; s < sides; ++s) {
wom.indices.insert(wom.indices.end(), {
back + s, front + s, back + s + 1,
back + s + 1, front + s, front + s + 1
});
}
// Back cap (-Z) fan.
uint32_t backCenter = addVertex(wom, {0, radius, -halfL},
{0, 0, -1}, {0.5f, 0.5f});
uint32_t backRing = static_cast<uint32_t>(wom.vertices.size());
for (int s = 0; s <= sides; ++s) {
float u = static_cast<float>(s) / sides;
float ang = u * 2.0f * pi;
glm::vec3 p(radius * std::cos(ang),
radius + radius * std::sin(ang), -halfL);
addVertex(wom, p, {0, 0, -1},
{0.5f + 0.5f * std::cos(ang),
0.5f + 0.5f * std::sin(ang)});
}
for (int s = 0; s < sides; ++s) {
wom.indices.insert(wom.indices.end(),
{backCenter, backRing + s + 1, backRing + s});
}
// Front cap (+Z) fan.
uint32_t frontCenter = addVertex(wom, {0, radius, +halfL},
{0, 0, +1}, {0.5f, 0.5f});
uint32_t frontRing = static_cast<uint32_t>(wom.vertices.size());
for (int s = 0; s <= sides; ++s) {
float u = static_cast<float>(s) / sides;
float ang = u * 2.0f * pi;
glm::vec3 p(radius * std::cos(ang),
radius + radius * std::sin(ang), +halfL);
addVertex(wom, p, {0, 0, +1},
{0.5f + 0.5f * std::cos(ang),
0.5f + 0.5f * std::sin(ang)});
}
for (int s = 0; s < sides; ++s) {
wom.indices.insert(wom.indices.end(),
{frontCenter, frontRing + s, frontRing + s + 1});
}
// Optional pillow box at +Z end. Sits flat on the ground and
// pushes a bit past the bedroll's front cap so it reads as a
// separate prop.
if (pillowSize > 0) {
const float pHalf = pillowSize * 0.5f;
const float pHeight = pillowSize * 0.5f; // squashed
addFlatBox(wom, 0.0f, pHeight * 0.5f, halfL + pHalf,
pHalf, pHeight * 0.5f, pHalf);
}
wowee::pipeline::WoweeModel::Batch batch;
batch.indexStart = 0;
batch.indexCount = static_cast<uint32_t>(wom.indices.size());
batch.textureIndex = 0;
wom.batches.push_back(batch);
float maxZ = halfL + (pillowSize > 0 ? pillowSize : 0);
wom.boundMin = glm::vec3(-radius, 0, -halfL);
wom.boundMax = glm::vec3(+radius, 2.0f * radius, +maxZ);
if (!wowee::pipeline::WoweeModelLoader::save(wom, womBase)) {
std::fprintf(stderr,
"gen-mesh-bedroll: failed to save %s.wom\n", womBase.c_str());
return 1;
}
std::printf("Wrote %s.wom\n", womBase.c_str());
std::printf(" bedroll : len=%.3f, R=%.3f, %d sides\n",
length, radius, sides);
if (pillowSize > 0)
std::printf(" pillow : %.3f cube at +Z end\n", pillowSize);
else
std::printf(" pillow : (none)\n");
std::printf(" vertices : %zu\n", wom.vertices.size());
std::printf(" triangles : %zu\n", wom.indices.size() / 3);
return 0;
}
int handleChimney(int& i, int argc, char** argv) {
// Brick chimney: rectangular shaft topped by a slightly-wider
// cap (the protective crown that throws rain off the masonry).
@ -7059,6 +7190,7 @@ constexpr MeshEntry kMeshTable[] = {
{"--gen-mesh-dock", 1, handleDock},
{"--gen-mesh-pergola", 1, handlePergola},
{"--gen-mesh-chimney", 1, handleChimney},
{"--gen-mesh-bedroll", 1, handleBedroll},
{"--gen-mesh-table", 1, handleTable},
{"--gen-mesh-lamppost", 1, handleLamppost},
{"--gen-mesh-bed", 1, handleBed},

View file

@ -232,6 +232,8 @@ void printUsage(const char* argv0) {
std::printf(" Pergola: 4 corner posts + 2 perimeter beams + N cross beams (open lattice top, no panel)\n");
std::printf(" --gen-mesh-chimney <wom-base> [width] [depth] [height] [capH] [capExtra]\n");
std::printf(" Chimney: rectangular brick shaft topped by a slightly wider rain-cap (default 0.45/0.45/1.8/0.10/0.05)\n");
std::printf(" --gen-mesh-bedroll <wom-base> [length] [radius] [sides] [pillowSize]\n");
std::printf(" Bedroll: horizontal closed cylinder along Z axis with optional pillow box at +Z (camp set dressing)\n");
std::printf(" --gen-mesh-table <wom-base> [width] [depth] [height] [legThick] [topThick]\n");
std::printf(" Table: flat top slab on 4 corner legs (default 1.6/1.0/0.85/0.10/0.06)\n");
std::printf(" --gen-mesh-lamppost <wom-base> [poleH] [poleT] [baseSize] [lanternSize] [lanternH]\n");