You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026-09-09 -- ShotPlanner.plan_respace(..., respect_locks=True) (core_utils/engines/shots/shot_plan.py). A locked gap keeps its own width through a respace, which is the point of locking one -- but it also meant the Shots panel's Apply Gap silently skipped those gaps with nothing to say why, and no way to insist. respect_locks=False spends the gap on every gap alike. The lock itself is untouched either way: an override is one operation ignoring a lock, not a way to quietly clear it, so the next respace without it honours the lock again.
2026-09-09 -- UvBudget plans a texture budget before anything packs (geo_utils/uv_budget.py). The planning sibling of UvPack: given per-group area, chart border length, chart count and authored density, it answers "how many maps of this size, and at what texel density?" and its inverse, in microseconds rather than the seconds a real pack costs. Demand is a closed form -- area*tpu^2 + pad*perimeter*tpu + 4*pad^2*charts, all three terms in px^2 so they compare directly against a page's map_size^2. The chart terms are what make it usable: 400 small charts and one large chart of equal area do not cost the same map, and a model pricing only area promises a density the packer cannot deliver on fragmented input. The gutter comes from MathUtils.calculate_uv_padding, so a plan made here and a pack run later agree on what a gutter is. Per-item density is what lets one solve cover content authored at different densities: the argument then scales every group together (1.0 = exactly as authored) instead of flattening them onto one number, which matters because flattening is usually wrong -- across seven sets of a measured production assembly the range is 11.2 to 126.2 px/unit, and one shared density would cost the lowest sets 127x their UV area. Fewest-pages uses First Fit Decreasing (<= 11/9*OPT + 6/9), fixed-page-count uses Longest Processing Time (fullest page <= 4/3 - 1/(3N) times optimal); both are O(n log n) and deterministic, because the plan is shown to a person who will re-run it and expect the same answer. padding_for floors the gutter at 2**mip_levels px and reports when it did -- the ratio rule scales the gutter with the map while mipmapping does not, so on small maps the mip floor binds. FILL_DEFAULT is calibrated, not chosen: planned page assignments were packed for real (xatlas via mayatk.UvUtils.pack_uvs, 2 pages at 1024) across varied primitives, 24 small fragments, and a lopsided set, and the worst-case achieved/predicted density ratio came out 1.158 at fill 0.50, 1.053 at 0.60, 1.009 at 0.65, 0.971 at 0.70 and 0.878 at 0.85. 0.65 is the highest value that never over-promises, so it ships -- 0.85 would have been ~12% optimistic, which on a 7-map job is a map that does not fit. A gutter-sharing parameter was built and then REMOVED, which is the more useful record: charging every chart border a full gutter strip over-counts where two charts abut, so a share term was added on that reasoning and swept jointly with fill (4 sharing values x 8 fill values, against real packs). Worst-case margin moved 4% across the whole sharing range against 32% across fill -- a second knob measuring nothing fill does not already absorb -- so it came back out. Infeasible requests report rather than returning a plausible-looking plan: an oversized item names itself, a chart count whose gutters alone exceed the budget says so, pages < 1 raises, and alternates are de-duplicated against the chosen row (asking for one page more routinely re-solves to the same count). level=True no longer overflows a page: LPT minimizes the FULLEST page, which is not the same as keeping every page inside capacity, and the gap is reachable rather than theoretical -- 5,5,4,4,3,3,3 fits three pages of 9 exactly while LPT puts 5+3+3 on one and overflows to 11 (122% fill, reported as feasible). Levelling is a presentation preference, so a levelled layout that does not fit is discarded in favour of the FFD one that does, and the row says it stayed uneven; pinned by a test that fails on the old behaviour. BudgetItem.payload is excluded from equality, so an item carrying a dict or a dataclass is still hashable -- a frozen type that promises to be a value should not fail on set(items) depending on what the caller stapled to it. BudgetRow now records whether its density is a multiplier (scaled) and renders itself accordingly, so a scaled solve never prints as "px/unit".