We have removed some bay collision primitives from being appended in the abstract. MhBayShape.appendLevelBracketPrimitives(CollisionPrimitive{} prims, Transform t=null) has been commented out. It was matching the holeStartZ value (first hole height) with the bottom of the lowest level's bracket. This logic is no longer wanted in the abstract.
public class MhBayShape extends MhBoxSnapperShape { /** * Append Level Bracket Primitives */ extend public void appendLevelBracketPrimitives(CollisionPrimitive{} prims, Transform t=null) { /* CUT THIS OUT Thu Apr 30 13:29:18 2026 // Dont let the level brackets go past first hole Layer bracketLayer = geometryLayer(owner, sLevelBracket); box bracketBound(rect(point2D0, w, d).grown(0.01), 0, holeStartZ - holePitch/2); CollisionPrimitive bracketPrim = mhGetBoxCollisionPrimitive(bracketBound, bracketLayer, t); prims <<? bracketPrim;//mhInstanceCollisionPrim(bracketPrim, null); * CUT THIS OUT */ } }
The group selection of level snappers in MhEditorSpace (e.g. Bay Editor) will now just return single selection. This is because the Bay Editor's level tab functions by modifying a single level. Also this helps avoid the user accidentally deleting/modifying several levels together.
public class MhLevelSelectionBehavior extends MhRowChildSelectionBehavior { Old: /** * GroupSelection */ public SnapperSelection groupSelection(MhSnapper snapper, Line mouseLine) { SnapperSelection res = exportLoadFromBehaviors(snapper, mouseLine, single=false); if (res.any) return res; return super(..); } New: /** * GroupSelection */ public SnapperSelection groupSelection(MhSnapper snapper, Line mouseLine) { SnapperSelection res = exportLoadFromBehaviors(snapper, mouseLine, single=false); if (res.any) return res; // Don't include root-parents' children for editor space. if (snapper.isChildSnapper) { if (snapper.space in MhEditorSpace) { return snapper.singleSelection(mouseLine); } } return super(..); } }
We have changed how the bay width stretch propagation action behaves now. Now when selecting an entire system and stretching a specific bay, all other rows will re-align such that their bays at the same column of the stretched bay will be aligned to the stretched bay.
1. Width propagation
The engine function MyRowChildWidthChangePropagateFunction now has the ability to align the x-position of all bays of the same columnId to the stretched bay. This guarantees that bays of the same columnId are aligned with each other, and helps MhRowChildrenAlignFunction to work properly.
public class MyRowChildWidthChangePropagateFunction extends MyRowChildChangePropagateFunction { Old: /** * Execute. */ public Object exec() { ... ?MhEngine engine = engine; angle rot = refEntry.toSpaceTransform(engine).rot.yaw; for (MhSnapper snapper in set) { if (MhEngineEntry e = snapper.engineEntry(engine)) { if (shouldPropagateToEntry(e, refEntry)) { double diff = refEntry.w - e.w; e.setW(refEntry.w); angle eRot = e.toSpaceTransform(engine).rot.yaw; if (eRot != rot) { // Get the sign direction to move this entry. double v = eRot.v - rot.v; v /= |v|; if (refEntry.pDiff == point0) { e.move((v*diff, 0, 0)); } } else { e.move(refEntry.pDiff); } } } } ... } New: /** * Execute. */ public Object exec() { ... for (MhSnapper snapper in set) { if (MhEngineEntry entry = snapper.engineEntry(engine)) { if (shouldPropagateToEntry(entry, refEntry)) { double entryDiff = refEntry.w - entry.w; entry.setW(refEntry.w); alignEntry(entry, refEntry, entryDiff); } } } ... } /** * Align entry. */ extend public void alignEntry(MhEngineEntry entry, MhEngineRowChildEntry refEntry, double entryDiff) { ?MhEngine engine = engine; if (shouldAlignToSameX(entry, refEntry)) { // Align the leftmost bottom corner of entry to refEntry x-wise. Transform entryToRefT = entry.toSpaceTransform(engine) - refEntry.rootParent(engine).transform(); box refB = refEntry.bound; box entryB = entry.localBound.transformed(entryToRefT); double xDiff = entryB.p0.x - refB.p0.x; entry.move((-xDiff, 0, 0)); } else { // Move by diff. angle refRot = refEntry.toSpaceTransform(engine).rot.yaw; angle entryRot = entry.toSpaceTransform(engine).rot.yaw; if (entryRot != refRot) { // Get the sign direction to move this entry. double v = entryRot.v - refRot.v; v /= |v|; if (refEntry.pDiff == point0) { entry.move((v*entryDiff, 0, 0)); } } else { entry.move(refEntry.pDiff); } } } /** * Should align to same x? * Will be further processed in MhRowChildrenAlignFunction. This aids alignment with anchorSnapper if given. */ extend public bool shouldAlignToSameX(MhEngineEntry entry, MhEngineRowChildEntry refEntry) { if (!alignColumn.?v) return false; ?MhEngineRowChildEntry entry = entry; if (!refEntry or !entry or refEntry.columnId == -1 or entry.columnId == -1) return false; return refEntry.columnId == entry.columnId; } }
Here is an example of a stretch.
Stretch bay to the right.

Result of engine function without aligning to same x-position.

Result of engine function with aligning to same x-position.
This logic currently only runs if propagating width during a stretch animation. It can be controlled with either the alignColumn argument or overriding bool shouldAlignToSameX(MhEngineEntry entry, MhEngineRowChildEntry refEntry) above.
public class MhBayRowEngineBehavior extends MhEngineBehavior { /** * Gather function args. */ public str->Object engineFunctionArgs(MhSnapper snapper, MhEngineFunctionRun func, symbol event="", MhPreprocessArgsEnv preprocessArgs=null, Object env=null) { ... case "rowChildWidthPropagate" : { if (env as MhSnapperChangedEnv) { ... bool alignColumn = func.name == "rowChildWidthPropagate" and animation.?isStretchAnimation(); return props { snapper=snapper, noticer=env.owner, rows=rows, moveRow=false, //moveRow=animation.?isStretchAnimation() }; alignColumn=alignColumn}; } } ... } }
1.5 Column grouping id
The engine function to assign entry column IDs used to be directly executed by MyRowChildWidthChangePropagateFunction but now it is appended by MhBayRowEngineBehavior before "rowChildWidthPropagated" is appended so it can be executed first. Column IDs are also reset during engine cleanup to avoid their values persisting.
public class MyRowChildWidthChangePropagateFunction extends MyRowChildChangePropagateFunction { /** * Execute. */ public Object exec() { ... if (refEntry.columnId < 0) engine.exec("systemColumGrouping", rows=rows, config=configuration()); } } public class MhBayRowEngineBehavior extends MhEngineBehavior { /** * Collect engine function to run. */ public void fetchEngineFunctionsRun(MhEngineFunctionRun[] functions, MhSnapper snapper, symbol event="", Object env=null) { super(..); ?MhSnapperChangedEnv env = env; switch(event) { case sChildShapeChanged : { if (str functionKey = childPropagateKey(env)) { if (functionKey == "rowChildWidthPropagate") { functions << MhEngineFunctionRun("systemColumGrouping"); } functions << MhEngineFunctionRun(functionKey); } } }
The logic within MhSystemColumnGroupingFunction has also been modified to be simpler. It now assigns an ID to each row child entry in ascending order of their x-position.
public class MhSystemColumnGroupingFunction extends MhSystemEngineFunction { Old: /** * Execute. */ public Object exec() { MhEngineRowEntry ref = null; int bayCount = 0; // Reference will be the one with most children. for (MhSnapper row in rows.collectedSet) { ?MhEngineRowEntry e = row.engineEntry(engine.MhEngine); // aisles have no col id. if (!e or e.isAisle()) continue; int count = 0; for (MhEngineRowChildEntry c in e.children(engine.MhEngine)) count++; if (!ref or count > bayCount) { ref = e; bayCount = count; } } if (!ref) return null; sorted double->(MhEngineRowChildEntry[]) entries(); for (MhEngineRowChildEntry entry in ref.children(engine.MhEngine)) { MhEngineRowChildEntry cEntry = entry.copy(); cEntry.modifyBuffer = null; box b = cEntry.bound(); //box b = entry.bound(); double x = b.p0.x.round(cMhDoubleMapPrecision); MhEngineRowChildEntry[] seq = entries.get(x); init? seq(4); seq << entry; entries.put(x, seq); } for (_, seq in entries, index=i) for (e in seq) e.columnId = i; for (MhSnapper row in rows.collectedSet) { ?MhEngineRowEntry e = row.engineEntry(engine.MhEngine); if (!e) continue; if (e == ref) continue; groupChildren(e, ref); } } New: /** * Execute. */ public Object exec() { for (MhSnapper row in rows.collectedSet) { ?MhEngineRowEntry rowEntry = row.engineEntry(engine.MhEngine); // Aisles have no col id. if (!rowEntry or rowEntry.isAisle()) continue; sorted double->(MhEngineRowChildEntry[]) entries(); for (MhEngineRowChildEntry entry in rowEntry.children(engine.MhEngine)) { box b = entry.unmodifiedBound(); double x = b.p0.x.round(cMhDoubleMapPrecision); MhEngineRowChildEntry[] seq = entries.get(x); init? seq(4); seq << entry; entries.put(x, seq); } for (_, seq in entries, index=i) for (e in seq) e.columnId = i; } return true; } }
2. Row children alignment
The stretched snapper is now passed into MhRowChildrenAlignFunction. It is used to get the index of the snapper to align all the children within a row to.
public class MhRowChildrenAlignFunction extends MhSystemEngineFunction { New: public MhSnapper anchorSnapper; Old: /** * Align X. */ extend public void alignX(sorted double->(MhEngineEntry[]) groupedEntries) { ... int index = { if (ignoreAnchor.?v) result 0; for (k, entries in groupedEntries, index=i) { for (MhEngineBoxEntry e in entries) { if (e in tempList) ?e = tempList.get(e); if (e.wDiff != 0) result i; } } result 0; }; ... } New: /** * Align X. */ extend public void alignX(sorted double->(MhEngineEntry[]) groupedEntries) { ... Int index = getAlignXIndex(groupedEntries); if (!index) return; ... } /** * Get align-x index. */ extend public Int getAlignXIndex(sorted double->(MhEngineEntry[]) groupedEntries) { if (groupedEntries.empty) return null; double[] xs = groupedEntries.keys; Int index = getAnchorIndex(); if (!index or index.v == -1) { // No anchor snapper index found. index = { if (ignoreAnchor.?v) result 0; for (k, entries in groupedEntries, index=idx) { for (MhEngineBoxEntry e in entries) { if (e in tempList) ?e = tempList.get(e); if (e.wDiff != 0) result idx; } } result 0; }; } else if (index.v > xs.count - 1) { // Anchor snapper index greater than row count, start from last. index = { if (ignoreAnchor.?v) result 0; for (k, entries in groupedEntries, index=idx, reverse) { for (MhEngineBoxEntry e in entries) { if (e in tempList) ?e = tempList.get(e); if (e.wDiff != 0) result idx; } } result 0; }; } return index; } /** * Get index of anchor snapper. * Currently only used for alignX. */ extend public Int getAnchorIndex() { if (ignoreAnchor.?v) return 0; if (cachedAnchorIndex) return cachedAnchorIndex; if (anchorSnapper) { ?MhEngine engine = engine; if (!engine) return -1; MhEngineEntry->MhEngineEntry oldTempList = tempList.shallowCopy(); tempList.clear(); // Group anchor row, and find anchor entry's index. MhEngineEntry anchorEntry = anchorSnapper.engineEntry(engine); MhEngineEntry rowEntry = anchorEntry.?rootParent(engine); if (!rowEntry) return -1; if (MhSystemEngineEnvironment env = systemEnvironment()) { MhEngineEntryBlock[] blocks = env.getPopulatedBlocks(rowEntry); MhEngineEntryBlock[] blocksToAlign = blocksToAlign(blocks); for (block in blocksToAlign, index=i) { sorted double->(MhEngineEntry[]) groupedEntries(); MhEngineEntry[] entries = mhYSortedEntries(block.entries, unmodify=true); // Group entries based on x-location. for (e in entries) { MhEngineEntry ogE = tempList.?get(e) ?? e; if (filter and !filter.accepts(ogE)) continue; box b = e.MhEngineBoxEntry.?unmodifiedBound(); double x = b.p0.x.round(cMhDoubleMapPrecision); MhEngineEntry[] es = groupedEntries.get(x); init? es(); groupedEntries.put(x, es); es << e; } cachedAnchorIndex = { for (k, entries in groupedEntries, index=i) { for (MhEngineBoxEntry e in entries) { if (e == anchorEntry) result i; } } result null; }; if (cachedAnchorIndex) break; } } if (!cachedAnchorIndex) cachedAnchorIndex = -1; tempList = oldTempList; } return cachedAnchorIndex; }
We've changed the enter key functionality for the class MhToolAnimationG2InputController so any classes that use it (e.g. MhSnapperToolAnimationG2) will adopt this new behavior.
Previously pressing enter would just end the animation but now enter performs a click action then release action to simulate a left click.
public class MhToolAnimationG2InputController extends ToolAnimationG2InputController { /** * Key enter. */ public bool keyEnter() { if (lastMouseInfo) { click(lastMouseInfo); release(lastMouseInfo); return true; } return super(); } }
When using a MhSnapperInsertToolAnimation which is an animation we typically use to insert racking snappers (aside from rows), the animation will usually have a vessel MhSnapperInsertToolVessel that is responsible for drawing the animation graphics.
Previously it would also draw graphics for other locations that are spread to as well based on MhSnapperSpreadVessel. This has now been turned off by default and it only draws the main candidate during animation even if it is spread to other locations. This change is to improve insert animation performance in large drawings as the large number of graphics causes a noticable slowdown.
public class MhSnapperInsertToolVessel extends Vessel { /** * Append insert 3D. */ extend public void appendInsert3D(MhSnapper z, Primitive3D[] prims, MhSnapperInsertEntry entry, FetchEnv3D env, bool showArrow) { if (mainCandidateGraphicOnly() and entry.snapper != mainCandidate()) return; ... } /** * Only draw entry graphic for main candidate. */ extend public bool mainCandidateGraphicOnly() { return true; } }