EDGEFAB / DOCS

Tests

Every test of EDGEfab, with the steps to run it again, what should happen, and each run’s result. Add a test here when it is first run. Release gate (the owner’s rule): no release until every test of the builds since the last release has passed. Untested or failed stops it.

In-game steps start with the Test timer: switch on REDnode’s Test timer app, press Start, and press Mark after each numbered step, so log lines can be tied to steps.

F1’s tests come first, then F0’s checks.

F1: prepare a model (smoke test, without VS Code)

The bundled core makes a prop from a GLB on disk, and REDbridge takes the result. Run after every change to src/core.

Steps.

  1. npm run build, then npm run smoke -- "<model>.glb" "<out folder>" on two Tripo models: one made with face_limit 20000, one without a limit (1.4 million triangles). F0’s runs gave both.
  2. Import the prepared GLB (<out>\<name>.prop.asset\source\raw\<name>\<name>.glb) with REDbridge’s project.import over base\environment\decoration\containers\barrel\drum\barrel_drum_a.mesh.
  3. The MCP server: npm run mcp:start, send initialize and tools/list.

Expected. The smoke test prints ok: three LODs, each part with POSITION, NORMAL, TEXCOORD_0, TANGENT and at most 65,535 vertices, the base at height 0, three texture files, and a view.glb beside the game GLB with the same parts and levels and the textures inside (added 2026-11-04, for Open in 3D). REDbridge returns ok: true with 3 submeshes. The server lists fab_make, fab_desk, fab_tripo.

Run 1, 2026-11-01: passed

Model Time Result
Tripo, face_limit 20000 0.2 s 0.65 m tall; LODs 19,021 / 9,510 / 3,794 triangles (10,980 / 6,307 / 3,151 vertices); color, ORM and normal written out. REDbridge: ok, 3 submeshes
Tripo, no limit (1,445,348 triangles) 2.7 s Simplified to 77,908 triangles (49,850 vertices) to fit, with a note saying so; LODs 77,908 / 38,954 / 15,572
MCP server   Started; lists fab_make, fab_made, fab_tripo (fab_made renamed fab_desk on 2026-11-02; it answers with an empty Desk)

Found on the way and fixed before this run: glTF-Transform’s normals() unwelds every mesh, and prune() drops UVs and tangents once the textures are out of the GLB (REDbridge then refused it for missing tangents).

Run 2, 2026-11-04: passed

F0’s drum again, after view.glb was added: the same three levels (19,021 / 9,510 / 3,794 triangles) and textures; view.glb (4.3 MB) has the nodes submesh_00_LOD_1, _2, _4 and one material with its color, ORM and normal textures.

Run 3, 2026-11-04: passed

After the turn to the game’s forward: test 8’s v3.1 vending machine is turned a quarter (note “Turned a quarter to face forward”), 0.85 m wide by 0.82 m deep (was 0.82 by 0.85), and drawn from four sides its front faces −Z in glTF. F0’s drum, also from Tripo, is turned the same way. Unverified in the game: that −Z in the GLB becomes the way a spawned or placed prop faces (to see with test 7’s run).

Run 4, 2026-11-04: passed

Glow: test 8’s vending machine with a glow map made from its bright, saturated painted areas (15.8% of its texture), emissiveFactor 1 and KHR_materials_emissive_strength 3. Prepare writes the map out (vending_glow_00_emissive.png) and lists glow with its colour, strength 3 and map; view.glb keeps emissiveTexture and the strength extension. Drawn from four sides with the map alone: the shelves, buttons, trim lights and a side panel glow, the body stays dark. The model’s file was written by glTF-Transform, yet it is still known as Tripo’s by its node names and turned.

Not yet tried: the extension in VS Code (Import a GLB, Generate with Tripo, the Desk view, Open in 3D in REDcode’s view), and fab_make with Tripo through the MCP server. Both spend nothing new until a Tripo generation (20 credits).

F1 check: Tripo’s model and its front

Which Tripo model F1 asks for, and which way Tripo’s models face, on a prop with a front. Not a release test.

Steps. Scripts in a session’s scratchpad, outside the repo: run.cjs reads the tripo key from the shared key store (never printed) and makes “a cyberpunk vending machine with a glowing screen on the front” three ways: (A) v3.1-20260211 with face_limit 20000, (B) P1-20260311 with face_limit 20000, both with texture, pbr and auto_size, at the same time; (C) POST /v3/mesh/decimate on A’s task (model “v2.0”, face_limit 20000, bake true). views.cjs draws each GLB from four sides (from +X, −X, +Z, −Z of glTF’s axes, textured, orthographic). Then npm run smoke on each.

Expected. Three textured models at real size; the front on the same side in each; each passes the smoke test.

Run 1, 2026-11-04: passed

  A: v3.1, face_limit B: P1 C: A, retopology with bake
Time, credits 127 s, 20 95 s, 40 313 s, 30 (on top of A’s 20)
Size (auto_size) 1.80 m tall, 0.82 × 0.85 m 1.80 m, 0.81 × 1.08 m 1.80 m, 0.81 × 0.84 m
Triangles, vertices 16,791, 12,455 17,909, 29,512 (many UV seams) 20,356, 20,662
Front +X +X +X
Looks Clean: glowing shelves, keypad, slot; plain sides and back Bottles on shelves, but the screen on a side, a blurred smudge baked on the back, a brown body A’s shape, but smeared texture patches on the sides from the bake
Smoke test ok, 3 LODs ok ok

Balance 1,520 before, 1,430 after (110 credits, which matches the three).

What it decides for F1: v3.1 with face_limit stays the default: the cheapest and the cleanest. P1 and retopology aren’t used for now. Text to model faces +X as multiview does, so prepare turns every Tripo model to the game’s forward. The front’s side can’t be told from an imported GLB, so imported models keep their facing and the modder turns them (F2’s size and placement on the page).

Also found: a task made with one Tripo key answers 404 to another key, even on the same account (the laptop’s vending machine could not be read from the PC). So a take or an edit records which key made it (a short fingerprint, never the key).

F0 check: Tripo text to model

What Tripo returns for a prop, so F1’s prepare step and EDGEmat #4 are built for it. Not a release test.

Steps. Two scripts, kept outside the repo (a session’s scratchpad): tripo-check.mjs reads the tripo key from the shared key store through REDnode’s tools/keys/keys.mjs and never prints it. It creates one task with POST https://openapi.tripo3d.ai/v3/generation/text-to-model, polls GET /v3/tasks/<id>, and downloads the output URLs. glb-inspect.mjs lists a GLB’s nodes, meshes, triangles, materials and images.

  1. node tripo-check.mjs <outDir> "a rusty oil drum" (model v3.1-20260211, texture and pbr on, no other options).
  2. node glb-inspect.mjs <outDir>/model_url.glb.
  3. Again with '{"face_limit":20000,"auto_size":true}' as a third argument.

Expected. A textured GLB within a few minutes, its credits shown, and a balance lower by that amount.

Run 1, 2026-10-31: passed

  Without options face_limit 20000, auto_size
Time, credits 133 s, 20 credits 117 s, 20 credits (the limit costs nothing extra)
Download 45.1 MB GLB, a WebP preview, the JPEG image Tripo made first from the words 3.7 MB GLB, the same two images
Geometry 1 node, 1 mesh, 1 primitive: 1,445,348 triangles, 736,445 vertices; POSITION, NORMAL, TEXCOORD_0; 32-bit indices 19,021 triangles, 10,980 vertices
Size 0.70 × 1.00 × 0.70, Y up, centered: scaled to 1 unit tall The same mesh bounds, with a node matrix scaling by 0.65: auto_size sets real size (0.65 m tall) on the node, not in the vertices
Material 1: baseColorTexture, metallicRoughnessTexture, normalTexture; factors 1 The same
Images, all 2048 × 2048 Color JPEG, ORM JPEG (glTF’s packing: occlusion, roughness, metal), NormalGL PNG (green up) The same
Extensions None None
Looks A good rusty drum (preview) As good at 19,021 triangles

What it decides for F1:

Also found: Tripo’s v2 API shuts down on 2026-10-31. Only v3 (openapi.tripo3d.ai, /v3/...) is used. Its model names for text to model: v3.1-20260211, v3.0-20250812, v2.5-20250123, P1-20260311, P2-20260801 (from Tripo’s MIT Python SDK, docs/API.md). Terms: the owner is on Tripo’s Pro plan, a paid plan, so the models are the owner’s (plan, read 2026-11-01). Still open: the price of a task with texture_quality detailed.

F0 check: a GLB imported over a game prop

Whether WolvenKit’s importer (through REDbridge) makes a game mesh from a Tripo GLB, with LODs, over a game prop’s mesh; and how game props get their collision. Not a release test. Done without the game; the in-game part is still open.

Steps. Scripts in a session’s scratchpad, outside the repo:

  1. prep.mjs (glTF-Transform 4.5.1, mikktspace 1.1.1, meshoptimizer 1.3.0, all MIT) on run 2’s GLB (19,021 triangles): bake the node transform, put the base at height 0 and centre it, make LODs by simplifying (submesh_00_LOD_1 full, LOD_2 50 %, LOD_4 20 %), generate MikkTSpace tangents.
  2. REDbridge 0.11.0 (WolvenKit 10.0.0, game 2.5) project.import of the GLB into a scratch project, with template base\environment\decoration\containers\barrel\drum\barrel_drum_a.mesh. bridge-call.mjs runs REDbridge directly, so its log shows (the MCP tool shows only ok).
  3. Read the new .mesh with cr2w_read; read three game props’ .ent files and the barrel’s .mesh.

Expected. A .mesh with three LOD submeshes at the GLB’s size, standing on its base.

Run 1, 2026-11-01: passed (without the game)

Found Detail
Tangents are required Tripo’s GLB as it comes is refused: “One or more geometry entries in GLTF do not contain Tangents data” (the MCP tool returned ok: false and the template copied, nothing more). The prepare step must make tangents
Node transforms are ignored The importer reads vertex positions as they are and only turns Y up into Z up. auto_size’s scale on the node is lost unless the prepare step bakes it
One primitive per mesh, and the limit is 65,535 vertices per submesh (not triangles) Read from WolvenKit’s importer. Each GLB mesh becomes one submesh; a mesh’s name and its node’s name should match
LODs come from the names A mesh named with LOD_1, LOD_2, LOD_4 or LOD_8 (one bit per level; LOD_1 required). Result: 3 submeshes, LOD masks 1, 2, 4 (10,980, 6,307, 3,151 vertices). The template’s LOD distances are kept (0, 8, 20 m), cut or extended to the number of levels
The pivot Tripo’s model is centred. Moved so its base is at 0: bounding box Z 0 to 0.65 m, X and Y ±0.21 m
The template’s leftovers The barrel’s two material entries (multilayered, ml_barrel_drum_a_masksset and a clean one), its two appearances and its collision stay. Every submesh uses the barrel’s material, which would draw wrongly on Tripo’s UVs, so Add to Project must replace the materials (EDGEmat #4) and remove the collision parameter
Garment support On by default in WolvenKit’s import arguments and not exposed by REDbridge; with no garment data it only warns. Harmless for props
How game props are solid Three game prop entities (sq021_barrels.ent, mq034_barrel.ent, q105_fluff_barrels.ent) use an entMeshComponent and an entColliderComponent with a physicsColliderBox (preset “Device solid visible” or “World Dynamic”, Kinematic). One (q301__barrel_drum_destroyed.ent) uses an entPhysicalMeshComponent, which takes its shape from the mesh’s meshMeshParamPhysics: there, a physicsColliderConvex held only as cooked PhysX data (compiledGeometryBuffer, 692 bytes, no plain vertices)

What it decides for F1: the prepare step does tangents, transforms, pivot, LOD names and the vertex limit. EDGEfab cannot cook PhysX shapes, so collision is made of shapes that need no cooking (box, capsule, sphere), fitted to the model. Still open, in the game: which of two ways is solid when spawned and when baked by EDGEbuilder: (A) an entColliderComponent with a fitted box, as the game’s props do, or (B) an entPhysicalMeshComponent whose mesh’s physics data holds a fitted box instead of the template’s cooked shape (EDGEfab #2’s shape). And whether the mesh looks right and switches LODs.

F0 check: which collision is solid in the game

The two ways left open above, side by side, on F0’s drum. Not a release test. Built 2026-11-04; not run yet (needs the game on the PC).

Built. A test project Projects\EDGEfab_Collision (outside this repo). Its archive tests\edgefab_collision_test.archive (4 files) holds, under edgefab_test\collision\:

  Entity Mesh
A drum_a.ent: an entMeshComponent and an entColliderComponent with a physicsColliderBox (half extents 0.214, 0.214, 0.325 m, raised 0.325 m), preset “Device solid visible”, Kinematic, as sq021_barrels.ent has drum_a.mesh: F0’s imported drum with its physics data taken out
B drum_b.ent: one entPhysicalMeshComponent, filter from the component (“Device solid visible”, Parent), Kinematic, as crate_red.ent has drum_b.mesh: the same drum, its meshMeshParamPhysics holding the same box instead of the template’s cooked convex shape

Both meshes keep the game barrel’s material, so the drums may draw wrongly: this test is about collision and LODs only. Made from WolvenKit JSON with REDbridge’s cr2w.fromJson, then project.pack.

Steps (Test timer: Start, then Mark after each step).

  1. Copy the archive into the game’s archive\pc\mod (or add it with EDGEdeck), start the game and load a save outdoors.
  2. Spawn A and B two metres apart in front of V (REDnode world.spawn with each .ent, from Claude Code).
  3. Walk into each, then jump onto each.
  4. Shoot each.
  5. Walk 25 m away, watching each drum’s detail (LODs at 0, 8 and 20 m).
  6. Later, in EDGEbuilder: place each in a scene and bake it, then walk into each again.

Expected. Each drum stands on the ground at its size. One or both stop V; the one that does is F1’s collision (EDGEfab #2 asks for B). Its detail drops as V walks away.