-
Notifications
You must be signed in to change notification settings - Fork 113
zone_painter_ui_stories
Status: design narrative (2026-07-21) for the zone painter UI milestones decided in
pipeline_max_design.md §14-paint ("UI strategy — one in-engine editor, no Qt tier").
Part 1 documents how the shipped interim tool (nel/tools/3d/zone_painter, keyboard+HUD)
is used today. Part 2 reimagines the same work as the fully immersive in-engine editor
flow. Part 3 grounds every new capability against what the paint core already proves.
"Currently" means the interim workhorse: the NeL viewer with hotkeys plus the headless script layer. The NeL-GUI editor shell, OpenXR, and WebXR presentations are decided but not built.
1. Paint a zone without owning 3ds Max.
As a landscape artist, I have terrain authored as NeL patch meshes inside .max source
files, and I want to texture-paint them without launching Max or the old in-Max painter
plugin, so that painting works on any machine (including Linux, even headless under
xvfb/Mesa). I run zone_painter myzone.max --bank desert.smallbank --save out.max,
paint, and get a .max back that is byte-faithful everywhere except the tile/color/
displace records I actually changed. The .max stays the single source of truth: the
unchanged export pipeline (zone exporter → welder → lighter → client) consumes my saved
file with no extra conversion step, and my edits survive stacked modifier chains because
the tool writes into the same carrier the exporter reads (topmost paint snapshot, else
the base patch object).
2. Paint one brick in the context of its neighbors.
As an artist working on a ligo brick or a zone in a multi-zone file, I want to see the
surrounding terrain while I paint, so that transitions read correctly across zone
borders. Neighboring frozen zones load as reference display: they join the landscape,
the border weld, and the tile-adjacency graph — so my transitions resolve against their
tiles — but they are never paint targets and their data is never rewritten. With
--lock-borders (or the L key) I can also protect my own border tiles against
accidental strokes.
3. Paint with the village in place.
As an artist texturing terrain under a settlement, I want the buildings, trees, and
lights visible while I paint, so that paths and dirt line up with what actually sits on
the ground. --include-meshes (or the flag my file already stores) displays every real
mesh in the scene — including XRef'd constructions like village buildings, resolved
through the database — lit by the scene's lights, while meta-geometry (collision meshes,
cluster volumes, PACS primitives, targets) is filtered out. Textures for both the
landscape tiles and the context meshes resolve automatically from the database layout,
seasonal variants included, so a fresh checkout renders correctly with no manual
search-path fiddling.
4. Keep painting verifiable in the pipeline.
As a technical artist or pipeline maintainer, I want painting operations to be
reproducible and checkable in CI, so that the paint step never silently corrupts source
files. The headless layer gives me --null-edit --verify-identical (prove the save path
is a byte-exact no-op), --paint-script (replay the exact same ops the mouse performs),
--screenshot before/after pairs for visual review, and checkseams-style mechanical
transition-legality checks — all gated on the corpus (ctest gate #25). An artist's
interactive session and an automated batch job go through one shared op layer.
5. Start a session.
As an artist, I launch the viewer with my .max and the ecosystem's tile bank; the tool
assembles the painting landscape exactly as authored (no export-time rotation/symmetry)
and drops me into an orbit camera around it, with a HUD showing my current mode, tile
set, brush, and group. My old plugin-era keys.cfg from the Max painter parses verbatim
if I want my muscle-memory bindings; otherwise sensible defaults apply.
6. Paint tiles. As an artist in tile mode (T), I right-click terrain to pick the tile set already under the cursor, or step sets with PgUp/PgDn/0–9. I paint with the left mouse; the tool automatically places the correct transition tiles between different sets, recursively repairing borders as I go — including across welded zone boundaries. I toggle 128/256 tile size with B, bias variety toward a tile group with G/V, grow the stroke area with +/−, and flood-fill a patch with F (or F6/F7/F8 for rotated fills). Ctrl+Z/Ctrl+E give me bounded undo/redo, and a save after undo is byte-identical to never having painted.
7. Paint vertex colors.
As an artist in color mode (C), I paint ambience with a falloff brush whose radius
(+/−), hardness (Home/End), and opacity (Insert/Delete) I adjust live against the HUD. I
can stamp through a grayscale bitmap mask — cycled with S or given via --brush-mask —
for texture in the strokes. Shared border vertices are blended once and written
identically to every zone that touches them, so my color work never cracks along seams,
even across zone borders (stronger than the old plugin, which could leave per-side
discontinuities).
8. Paint displacement. As an artist in displace mode (D), I select one of the 16 displacement noise maps and paint surface roughness with the same area-brush sizes as tiles, with fills available for larger regions, and undo covering it all.
9. Finish and hand off.
As an artist ending a session, I press ESC and the tool saves to the --save target —
never in place, so my input file is untouched until I'm satisfied. What lands in the
output is exactly my paint edits and nothing else, ready for export.
Known friction (accepted, interim): no visual tileset thumbnail browser yet — sets are picked by number or by right-click-picking existing terrain — and brush parameters live on hotkeys rather than panels. Those are precisely the first flat-screen NeL-GUI editor milestone. Scene ambient in the context display also still uses a default value pending the Max-side probe of the undecoded RenderEnvironment class.
The same work, reimagined as the in-engine editor. Where a story leans on something the current core already proves (or explicitly doesn't yet), Part 3 carries the grounding.
1. Open the tool, not a file.
As an artist, I launch zone_painter with no arguments and never see a terminal. The
tool finds the graphics repository on its own by walking up from the working directory
looking for the quick-start layout marker (the .nel sibling folder that anchors the
code/graphics/leveldesign checkout). If I launched it from somewhere unanchored, a
NeL-GUI folder browser appears at startup and I point it at the graphics folder once;
the choice is remembered. Power users and scripts can still pass the folder as the
single optional argument. Everything after this point is in-engine UI.
2. Choose what world I'm working in.
As an artist, after startup I'm asked one question: ecosystem or continent. Picking an
ecosystem (jungle, desert, lacustre, forest, primes — discovered from what's
actually present in the graphics repo, not hardcoded) means ligo-style brick authoring
and selects that ecosystem's smallbank and texture roots automatically. Picking a
continent (snowballs-style authoring) means working on that continent's zone set
with its bank. I never manually locate a .bank file or type a --search-path again —
the selection is the configuration.
3. Flip seasons at any time. As an artist, I can toggle between season variants (or whatever variant sets the graphics repo actually contains — discovered from the postfixed texture families, not assumed) from a persistent UI control mid-session. Only presentation changes: tile indices, colors, and displacement are variant-independent data, so the toggle re-resolves and reloads the bank and context textures live while my paint state stays untouched. I paint a transition once and immediately check that it reads correctly in winter and summer.
Ecosystem board story (refined 2026-07-22, IMPLEMENTED ui M24a-d). List-first startup stays (picking the first brick from the browser list just skips a step); on open, the brick's saved neighbor hints — horizontal, vertical AND diagonal — auto-load as read-only frozen context. Beyond that the board is the customization surface: empty cells split into UNLOCKED (hor/ver adjacent to an occupied block, or named by a saved hint — diagonal contact alone does not unlock, matching the ligo border rule) rendered as light open wells, with hint-named wells showing the remembered neighbor's name dimly, and LOCKED (dim, inert to clicks, still a drag target); clicking an unlocked well offers a menu with the cell's SAVED-NEIGHBOR hint as one-click "Open '' (read-only)" / "Open '' editable" at the top, then Open editable… / Open read-only… (brick picker, session-hinted bricks sorted first) / Place instance. The ecosystem path fully supports editing multiple files simultaneously (open files place on the board with derived footprints, close/save/toggle-RO per cell — the continent multi-file machinery; per-file dirty + save-all; hints stamp PER FILE with offsets relative to each file's own cell, and record the other open files too, so any brick reopens with its saved working set) and instancing any open brick multiple times (instances carry a source basename; clones share the source's carriers so painting the source repaints every instance live). Context bricks rotate/mirror like instances (block-center pivot; the transform rides the hint format as "dx,dy,r,m:name", emitted only when non-default so old payloads stay byte-identical). Drag = move, Ctrl/Shift-drag = copy (file-manager idiom) for instances, context bricks, and open files; illegal drops refuse with the collision reason. Opening a brick whose footprint does not fit at the requested square auto-shifts to the nearest fitting cell (spiral search) and loads anyway — the user drags it into place afterwards. Nothing appears by itself beyond the hint auto-load; every menu add offers both editable and read-only.
4. Continent: pick zones from a map grid. As a continent artist, I see the continent as a 2D grid browser — cells laid out by zone footprint, with thumbnails where available. I click cells to mark zones editable and mark others read-only; when I mark a zone editable, its neighbors are auto-suggested as read-only so my border edits always weld and transition against real adjacent data. Read-only zones are exactly the current frozen-zone semantics: they join the landscape, the border weld, and the tile-adjacency graph, transitions resolve against their tiles, and their files are never written.
5. The grid is a lens, not a container. As an artist, I can trust the grid even though zone geometry doesn't respect it: zone AABBs don't have to match the grid, edges can be jagged or extend well past the cell for conveniently modeled complex shapes. Selection therefore resolves by actual geometry footprint, not by cell name — a zone whose patches spill into three cells is offered from all three, and the read-only suggestion ring is computed from real patch-AABB overlap and weld adjacency rather than from grid neighborhood. I never lose edge context because a neighbor's geometry happened to cross the line.
6. Ecosystem: compose a scratch layout from bricks — with real footprints.
As a ligo brick artist, I also get a grid — but it's a scratch workspace, not the world,
and it understands that a brick occupies a footprint, not a cell. I can browse the
ecosystem's brick families and drop any .max file into any cell at any point during
the session, mixing my work-in-progress brick with existing reference bricks loaded
read-only around it. When I drop a brick, the tool derives its W×H extent and occupancy
mask from the ligo boundary edges of that file’s single editable zone in the .max
(by design one paint target per normal brick; exporter-identical mask algorithm). A
.ligozone is only ever an optional test cross-check when a prior export happens to
exist; authoring never depends on build outputs. The brick claims exactly those cells —
including L-shapes and masked-out holes. Placement snaps by whole footprint; occupied
cells refuse overlapping drops; the grid browser draws multi-cell bricks spanning their
cells with the mask visible, so I always see which cells a transition or corner piece
actually owns.
6b. Geometry still overhangs the footprint. As an artist, I can rely on the distinction between the logical footprint (the cell mask — what ligo placement reserves) and the geometry (which can be jagged, off-grid, and extend past the footprint for conveniently modeled complex shapes). The grid reserves cells by mask; the viewport renders true geometry; picking and welding work on geometry. Only the logical mask participates in placement legality — a neighbor's overhanging silhouette never blocks a cell it doesn't own.
7. Instance a brick against itself — edge-matched by footprint. As a brick artist doing the initial paint on a repeating zone, I place the same brick as multiple true instances — one shared backing, so a stroke on any instance appears simultaneously on all of them. Side-by-side self-instancing is how I verify a repeating edge actually repeats: the brick's edge welds against its own opposite edge in the layout, the tile-adjacency graph spans the seam, and the transition solver and color-closure rules resolve across it — so painting near the edge keeps both sides consistent by construction instead of by eyeballing. For a multi-cell brick, the self-tiling placement offers the legal adjacencies: instance offsets are whole-footprint translations (and the ligo rotations, 0/90/180/270, which rotate the mask with the geometry), so the brick's right edge cells land against its own left edge cells exactly as the world editor would place them — a 4x2 brick tiles against itself the way it will actually tile in a continent, not just edge-to-edge in the 1x1 sense.
7b. Edge inventory for multi-cell bricks. As an artist finishing a brick, I can see its border as ligo will see it: the footprint's boundary decomposed into per-cell edge segments (which is what must match neighboring bricks cell-by-cell in the world editor). Self-instancing is per-edge-pair: I can ask the layout to butt edge A of the footprint against edge B — including different edges of the same brick — to verify every advertised edge combination paints and welds cleanly, not only the trivial left-against-right case.
7c. Thumbnails come from the files themselves.
As an artist, the zone and brick browsers show previews read from each .max file's own
embedded OLE thumbnail (the SummaryInformation PIDSI_THUMBNAIL DIB — the same preview
3ds Max shows in its file dialogs), falling back to the exported zonebitmaps/ png when
a file has none. When I save from the painter, it re-renders a top-down view of the zone
and writes it back into that thumbnail slot, so every browser — this tool's, Max's own
file dialog, anything OLE-aware — shows the current painted state. CLI batch saves keep
byte-identity by default (thumbnail write is opt-in there); the interactive save updates
it by default with a checkbox.
8. Many files, one session, honest saves.
As an artist, I work across several editable .max files at once with per-file dirty
indicators, and save all (or individually) from the UI. Each file is written back
independently and byte-faithfully — only the carriers I actually painted change — and
the tool still never silently overwrites: in-place saving stays an explicit, confirmed
act. Undo history spans the session but respects file boundaries on save.
9. Everything on keys is also on panels.
As an artist, every toggle and parameter that exists as a key binding is now also
visible, discoverable UI: mode (tile/color/displace), brush size, hardness/opacity
sliders with live preview, brush-mask picker, tile-group bias, 128/256, lock-borders,
fill with rotation. The keys remain (plugin-era keys.cfg still parses); the panels
make the tool learnable without a cheat sheet, and the HUD state I used to read from
text becomes controls I can click.
10. A real tileset palette. As an artist, I pick tile sets from a thumbnail palette rendered live from the bank's own textures (which already resolve in-tool) instead of memorizing set numbers — right-click-picking from terrain remains as the fast path. The palette follows the season toggle, so I'm always choosing from what I'm currently seeing.
11. Context visibility I control. As an artist, the context display's fixed policy becomes my checkbox panel: buildings and XRef'd constructions, hidden nodes, collision meshes, cluster/portal volumes, PACS primitives, per-class toggles with live counts.
12. The same session, immersive. As an artist, the flat-screen editor is the daily driver, but the identical session — same working set, same panels reprojected, same op layer — opens in OpenXR for the genuinely spatial passes: judging displacement at eye level, placing transitions, painting vertex-color ambience with a controller ray, which is just another ray into the existing pick path. The WebXR build makes the same editor the browser showpiece.
13. The contract underneath never changes. As a pipeline maintainer, I keep the guarantee that no correctness depends on any of this UI: every action the panels perform goes through the single headless op layer, a session remains replayable as a paint script, and the null-edit, round-trip, and seam-legality gates keep running against the same core the immersive editor drives.
- Shared paint backing across instances: pristine carriers are keyed by leaf/rpo POINTER; instanced modifier stacks across nodes already share one paint state (P3b). Story 7's "stroke on any instance shows on all" is the existing semantics.
- Read-only/frozen zones: participate in landscape, weld, and the metaTile adjacency graph; never written (P3a). Stories 2 and 4 are the same mechanism at different granularity.
- Closure-based seam continuity: border vertices blended once, written identically to every writable slot (P3c) — extends across instance seams unchanged once the graph spans them.
- Season texture remap: the DBPATH + season-postfix resolution machinery exists (P3d follow-up); story 3 invokes it live instead of once at startup.
-
Context display + per-class stats:
SContextStatswas explicitly built as the data model for story 11's visibility panel (P3d follow-up TODO). -
The headless op layer: single-path ops,
--paint-script, null-edit and corpus gates — story 13 is the standing §14-paint validation contract.
-
Instance-aware display: one shared paint state rendered at N world transforms,
per-instance pick routing (transform the ray, not the data), and a transform-aware
weld/adjacency graph so a zone can neighbor itself. The ligo transform display space
(
transformDesc/symVector) is plumbed but deliberately unassembled since P3c — this milestone assembles it, including rotated/mirrored instances. - The footprint model: exporter-identical W×H + occupancy-mask derivation at load time, footprint-aware placement legality in the grid, mask rotation folded into the instance transforms, and per-cell edge segmentation for the edge inventory (7b).
- Grid browsers: continent zone selection by geometry footprint (story 5's lens-not-container rule), ecosystem scratch layout, thumbnails.
-
Multi-file session management: N editable
.maxfiles, per-file dirty state, save-all, session-spanning undo that respects file boundaries. -
Startup flow:
.nelquick-start-layout autodetection, NeL-GUI folder browser, ecosystem/continent selection driving bank + search-path configuration. - Tileset thumbnail palette (the known interim friction, story 10).
- Presentations: NeL-GUI panel shell (milestone 1), OpenXR backend for the NL3D stereo framework + controller-ray picking (milestone 2), WebXR via emscripten (milestone 3) — per the §14-paint decision.
-
CZoneBankElement(nel/include/nel/ligo/zone_bank.h:59) carries_SizeX/_SizeYplus a boolean occupancy mask over the W×H cell rectangle (setMask) — a brick isn't just "3x2"; it can be L-shaped or have holes. The size rides as a"size" = "WxH"category string (plus"square"and"filled"categories), parsed back byconvertSize()(nel/src/ligo/zone_bank.cpp:83). -
Producer: the ligo Max export (
nel/tools/3d/ligo/plugin_max/script.cpp, ~1098–1267) writes the.ligozonebank element. The mask comes from one of two sources: the realCZoneTemplate::getMask— derived from the ligo boundary edges flagged in the.maxand snapped to the cell grid — or the forced bounding fallbackgetSquareMask(geometry AABB ÷ cellSize, all cells true). -
Source of truth for the painter: always the open
.max..ligozoneis a build output of the ligo export process (absent in a fresh workspace checkout; initial paint happens before any export). Depending on that artifact for authoring would be incoherent — the painter re-derives the mask exporter-identically from the live patch edges (or AABB /USE_BOUNDINGBOX).--check-ligozoneis test/CI parity only. -
One editable zone per
.maxis by design (M11b): normal authoring (material / special / continent) paints exactly one eligible RklPatch; other patches in the file are RO context or embeds, not co-targets. Board footprint follows that same zone. Exception:zonetransition-keeps all non-frozen slots (export’s 9-cell scheme).--all-zonesis only an escape hatch. This is not an incomplete “union of every RklPatch” gap — it is the product model (one.zone/ one.ligozoneper protocol brick). - Geometry vs footprint: the mask is the logical placement unit; the geometry legitimately overhangs it (jagged, off-grid edges for convenient modeling). Placement legality reads the mask; welding and picking read the geometry (stories 5/6b).
- The
.maxOLE container carries aSummaryInformationproperty-set stream (kept verbatim by the pipeline_max save path, never parsed by the typed pipeline) holding the standard OLE thumbnail property (PIDSI_THUMBNAIL) as a DIB — e.g. 128×115 24bpp inzonematerial-converted-200_dz.max. This is the preview 3ds Max renders into its file-open dialog on save. - Reading it gives the browsers always-current previews for ALL files (the exported
zonebitmaps/pngs only coverconverted-*names). Writing it at save time (top-down ortho render of the zone bbox) keeps the preview in sync with the painted state and is visible to Max itself. Byte-identity gates unaffected: null-edit and default CLI--savenever touch the stream; thumbnail write is interactive-default / CLI opt-in.
- 3ds-Max-style horizontal toolbar at the top of the viewer: square buttons, text-only until icons exist, grouped with separators: board / save /// undo / redo /// tile / color / displace /// current season (the season button shows the active season and opens a context-menu selection instead of blind cycling). These global actions, edit mode switches, and display switches move OUT of the right panel; the panel keeps the current-mode editing functions (tile set, brush controls, lock borders, fill, palette toggle). Everything still routes through the shared key/UI bridge handlers. The toolbar drags by a slim grip on its LEFT edge, not a top title bar — the Ryzom client's toolbars (hands/gestem bar etc. in the client interfaces_v3 XML) are the reference idiom.
MaxScript-like Lua scripting over the embedded NLGUI Lua state. An ADDITIONAL option, never the primary route: everything scripting can do, the UI does through the same single op layer, and vice versa.
User stories:
S1. The landscape artist gathering reference (recorder).
As an artist who doesn't write scripts, I toggle Record, do a few strokes, change a
brush setting, save — and the script editor window fills with the painter.* calls
equivalent to what I just did. I copy two lines to repeat an exact stroke on another
zone, or just read them to learn the vocabulary. Recording is for reference first —
but what it produces runs verbatim if I do run it.
S2. The technical artist batching repetitive work.
As a TA, when 40 bricks need passable set, or every zone of a family needs the same
fill and a save, I write ten lines: iterate painter.zones(), filter by name, call
painter.setZoneProp/painter.fillTile, painter.saveAll(). The ops go through the
exact paths the UI uses — undo works, dirty tracking works, byte-faithfulness holds.
S3. The power user with their own board workflow (deep edge).
As a power user, my startup script composes my session the way I like — my favorite
brick, the two neighbors I always paint against, a 2x1 instance for the seam check —
using painter.openZone, painter.placeContext, painter.placeInstance. It's my
option, not the tool's default flow: without a script the boards work exactly as
designed.
S4. The pipeline maintainer preparing deterministic fixtures.
As a maintainer, I produce test input/output scenes reproducibly: a Lua script seeds,
paints, sets properties, saves — headless via --lua-script — and byte-gates compare
outputs. Same script, same bytes, forever. The existing paint-script vocabulary stays;
Lua supersets it with control flow.
S5. The tool developer prototyping. As a developer, I prototype a new tool idea (a smart brush, a seam-repair pass) as a Lua function driving existing ops before committing to C++. The script editor is my REPL against the live session.
S6. The plugin-era veteran.
As a MaxScript user, the verbs feel familiar (paint, fill, select, properties as
get/set), while the syntax is honest Lua following the Ryzom client's conventions
(camelCase, painter namespace table) — no invented DSL.
S7. The long-script author (pump).
As a script author running a minutes-long batch, I call painter.pumpUI() inside my
loop: the viewport and progress stay alive, input is locked except Cancel (the Script
window's CANCEL button and ESC both request it; pumpUI() returns false once
requested), and the pump only actually renders when >100 ms of processing has elapsed
since the last one — calling it every iteration costs nothing.
Design pins: single op layer (script calls = the same shared handlers as keys/UI;
recorder instruments that layer and skips script-originated calls); recorder output =
valid runnable script; script editor window (multiline edit + Run + output log +
Record toggle); --lua-script file.lua headless CLI; optional user startup script;
byte-gates: lua paint+undo+save == null-edit, lua op == identical bytes to the same
paint-script op.
IMPLEMENTED (ui M23a-d, 2026-07-22): script_api.{h,cpp} binds the painter
namespace over the extracted zpExecScriptOp (the --paint-script executor — Lua ops
byte-equivalent, gated). Surface: paint/fill/clear/rotate for tiles-color-displace
(paintColor/fillColor take the optional blend arg), colorBrush + tileStroke (mouse-path
strokes incl. brush recursion via the new tstroke op; optional big flag replays
256-mode strokes faithfully), undo/redo/seed/checkSeams, setZoneProp/getZoneProp,
zones(), save/saveAll/screenshot, pumpUI (100 ms-gated, ESC or CANCEL button cancels),
openZone/closeZone (board sessions), setRecording/recorderText. State setters split by
backing: core-backed ops setLockBorders/setMaskMode (new lockborders/maskmode
script ops — recorded scripts replay headless), viewer-bridge absolute setters
set256/setHardness/setOpacity/setRadius plus
mode/tileSet/brushSize/group/displaceIndex/brushColor/brushMask/season. The recorder
covers all of those as absolute painter.* lines (fills, season, mask, color included).
While a pumped script executes, every zone-painter action handler is locked except
CANCEL. Script window: Recorder pane + multiline editor + Output pane +
RUN/REC/REC>ED/CLEAR/CANCEL, SCRIPT toolbar button. User startup hook: --startup-lua,
default ./zone_painter_startup.lua (keys/vars cfg convention), runs once at
viewer-session start, errors never abort.
- The Painter panel is a 3ds-Max-style ROLLOUT column: the NLGUI container-tree idiom (the GUI sample's Contacts window — parent container, openable tree-child containers with collapsible headers, scrollable parent). Sections: Session / Tiles / Brush / Fill / Displace / Properties. The current mode gates which rollouts are VISIBLE at all (non-applicable sections are fully hidden, not collapsed-but-present); within the visible set each rollout is user-collapsible, with per-rollout open state remembered across mode switches. Future-proofs the panel for complex brush libraries and further tool sections.
- A FOURTH editing mode alongside Tile/Color/Displace: Prop (property editing). Selection behaves like modern 3ds Max object selection: hover = thin outline of the zone under the cursor; click = selection with a THICK outline around the visible mesh boundary (cel-shading-style silhouette against background/non-same meshes, CPU- computed; the zone's boundary-edge outline is the accepted fallback if a true screen-space silhouette is infeasible). Only read-write editable zones are selectable.
- The mode-specific panel section edits the selected zone's export properties — the editable-in-Max set: Ligo Rotate (0-3), Ligo Symmetry, Passable, Use Bounding Box — written as the same Max-shape appdata the exporters read, plus read-only info (name, footprint size/source, eligibility). USE_BOUNDINGBOX edits re-derive the footprint live. Property edits mark the file dirty and ride the normal save paths.
- Undo/redo fully covers property edits on the SAME stack as paint ops.
- The grid board is the persistent working-set screen, not a one-shot launcher: the painting viewer has a way BACK to the board with the session intact; cells show live state (closed / open-editable / open-read-only / instance-of-X, with dirty markers); per-cell actions cover open, close, and save-individual without tearing the session down. Multi-select and save-all remain.
-
One editable zone per
.maxby design (exporter-faithful eligibility, M11b): a file with multiple RklPatches still has a single paint target for material/special/ continent (name-matched or first eligible); scratch/leftover/embedded zones stay RO context. Transition bricks are the multi-slot exception. Escape hatch--all-zonesrestores pre-M11b open-everything when needed. - Instancing is board-driven: any opened zone can be placed as an instance at any cell, with per-instance rotation (0/90/180/270) and mirroring — assembling the transformDesc/symVector display space, with paint picks inverse-mapped through the instance transform. The M4 fixed NxN self-tile layouts are retired: ligo bricks have different edge situations per side, so deliberate edge-against-edge placement (including rotated) is the real repeating-edge workflow; uniform NxN tiling was a stopgap. Rotation and mirror are relative to the WHOLE footprint block of cells the brick occupies (not per-cell, not the geometry AABB center): a W×H brick rotated 90° occupies an H×W block on grid-aligned cells, and mirror flips across the block axis — the transform pivot is the footprint-block center so instances always land on the grid.
- Build outputs (
.ligozoneetc.) are NEVER relied on for authoring — footprints, eligibility, and neighbor hints all come from source.maxfiles. A prior export's.ligozoneis only an optional mechanical cross-check of the same derivation. - The legacy 9-zone brick files embed frozen COPIES of the 8 neighbors (stale snapshots from the old single-file Max workflow). In board-driven sessions these are NOT displayed (the board selection is the sole authority on what loads — stale copies would overlap the real neighbor files); the legacy direct-.max CLI keeps them.
- Instead they serve as HINTS: board neighbor suggestion priority is (1) new appdata neighbor hints written by the painter on save — ONE appdata entry holding a versioned variable-length list of (dx,dy -> basename) pairs, offsets in cells relative to the zone's footprint origin (multi-cell zones have more or fewer than 8 adjacent positions, so no fixed slot count) — a zone reopens with the working set it was painted against, including for ecosystem bricks, (2) names parsed from legacy embedded copies, (3) continent grid-name derivation. New appdata ids are registered in export_ids.h after a repo-wide clash scan.
-
Source units drift (found 2026-07-22): some corpus .max sources are authored at
a different numeric unit scale with NO distinguishing metadata in the file (snowballs
5_AC.maxis 39.37x — inches — while its Config/SceneImpl chunks match the meters siblings byte-for-byte; the shipped 2002 reference zone sits at authored/39.37). Continent context loading therefore sanity-checks placement: a context zone whose center lands farther than 3x the primary span gets the known unit ratios tried both ways (in/cm/ft/mm), a match rescales its display geometry about the origin with a warning, no match drops the file as context (hints keep naming it). The camera and orbit hotspot frame the EDITABLE zones only (plugin selection-center faithful), so a stray context file can never blank the session view. Paint data and saves are unaffected — context files are read-only and the rescale is display/weld-side only.
- The continent grid browser is a true board: fixed-size square cells positioned by zone coordinate, scrollable on BOTH axes, rendering only used cells plus the empty fringe — every empty cell horizontally or vertically adjacent to a used cell (no diagonals — a diagonal contact alone doesn't decide a ligo border type) shows as a visibly empty square (non-interactive placeholder), nothing beyond the fringe exists. Sparse continents thus show their actual silhouette. Visual idiom: the minesweeper cell-grid pattern from nl_sample_gui (the large-board variant), not bespoke line-drawing. Used cells carry the zone name and (once wired) the embedded thumbnail.