Level Modeller devlog - July 12 to August 8
Four weeks of work on PapaiArt Tools - Level Modeller. Less flash this time, more foundation: the boolean result got noticeably cleaner, saved maps got a lot smaller, and a handful of long-standing annoyances in the editor are gone.
Spiral staircases
The Staircase primitive now has a Spiral switch. Turn it on and the steps wind around the object's local Y axis, with controls for the total turn (Sweep deg, signed - the sign picks the handedness, and anything past 360 degrees is fine for multi-storey towers), Inner R, Tread Thk, a centre pole (Pole R, 0 for none) and the arc resolution per step.
Each step is an annular sector: radius from inner to inner+width, spanning one step's slice of the turn, rising height/steps. Because the sectors never share an angle, the treads can never intersect each other whatever the thickness - which is what keeps the result a valid solid for booleans. Set the tread thickness equal to the per-step rise and the soffits meet the tread below, so it reads as one continuous helix; leave it thinner and you get the classic open-riser look. Face ids match the straight staircase, so per-face texturing carries over between the two modes.
Cleaner booleans
The cleanup pass that merges the raw boolean output back into flat faces used to give up on faces with several holes - a floor with a row of subtracted pillars, the single most common shape in a level. The old code bridged each hole to the boundary by casting a ray, which crosses a nearer hole when there is more than one; the merged outline became self-intersecting, the pass threw the face away, and the biggest surfaces in a scene silently kept their raw boolean soup - thousands of triangles where hundreds do.
Holes are now spliced in rightmost-first, each bridge picked by an exact visibility test. On top of that, the cleanup tolerances are scaled by the scene's extent instead of being absolute: at coordinates in the hundreds, a fixed tiny epsilon sits below float noise, so vertices that genuinely lie on an edge stopped being recognised as such, and big levels degraded exactly where you'd least want them to. There's a headless test for the pass now.
Smaller maps, and saving is exporting
A saved .lmmap carries its baked geometry inline, and that block is what the runtime consumers actually load. It's now stored welded and indexed - corners merge only when they agree on every attribute, so box corners and lightmap chart seams still split correctly - which cuts a real level's vertex count to about a third. That's a smaller file and less vertex bandwidth in-game.
Saving also rebuilds the geometry first if it's out of date, so a map edited and saved without a re-bake no longer stores a stale mesh (or none at all, if you saved before the first build). Older maps in the previous layout still open.
A texture library that survives moving a project
Adding or removing a texture used to churn the whole GPU library mid-frame - which could hang the app, because descriptors the current frame was still drawing with were being destroyed. The sync is now incremental and runs at frame start: unchanged entries keep their images, only new ones upload. Add/remove is also no longer treated as a geometry change, so it doesn't trigger a multi-second re-bake on a large level.
Library paths are stored relative to the project root when that's actually possible (a pick from another drive keeps its absolute path instead of silently resolving to nothing), the .project file is rewritten on every save so later additions aren't missing from it, and a project copied to another machine now re-anchors its root from the offset the file itself carries - no guessing, and a candidate is only adopted if it exists on disk. When a root really is gone, the manager says so in place instead of just rendering everything as the fallback checker.
Level Editor maps
Importing a Level Editor map accepts the new .pamap extension alongside legacy .json saves - the same document either way. The vendored geometry core was also re-synced: the old snapshot predated per-wall UV offsets and Tile Mode, so imported walls could be misaligned against what the Level Editor's own 3D view showed.
