Autosave Resources

There will also be a text file for each autosave in the cmWritable/autosave folder. This file lists the resources used by the corresponding autosave.

Inside cmWritable/externalFormatterResources, resources are grouped by drawing name so that resources can be shared by all the autosaves for that drawing, eliminating redundant resaving of the same resources.

Floating-point line widths

Now that new calculations are performed when obtaining the width of a LineType, developers might have to tune the width values used in their non-unit width style LineTypes (eg. variable and real).

Invalid Filename

In cm/io/util/urlCheck.cm, use checkWindowsFilename instead of the deprecated isValidPath

Variants Constraints

cm.abstract.dataSymInterface

Interfaces related to Vx expression validation takes additional vendor argument.

// DsiExprValidationEnv (dsiExprValidationEnv.cm)
Old: public constructor(VxRuntime rt, dsiConstraintExpressionType exprType, str expr, DataCatalog cat)
New: public constructor(VxRuntime rt, dsiConstraintExpressionType exprType, str expr, DataCatalog cat, str vendor)


//dsiVariants.cm
Old: public bool dsiValidateConstraintExpr(DataCatalog cat, dsiConstraintExpressionType exprType, str expr, StrBuf error=null)
New: public bool dsiValidateConstraintExpr(DataCatalog cat, dsiConstraintExpressionType exprType, str expr, str vendor, StrBuf error=null)

custom.dataCatalog.builder

Constraints-related Cards (Tabs) in Catalog Creator are placed between Geometry and Validation Cards. Take note of the sort keys used.

Geometry         // 700 (no change)
Constraints      // 720 (added)
ConstraintTables // 725 (added)
VariantsTables   // 727 (added)
Validation       // 745 (no change)

custom.dataCatalog.builder.constraints

Interfaces related to Vx expression validation takes additional vendor argument.

// DcDBBuilderConstraintsCard (dcDBBuilderConstraintsCard.cm)
Old: extend public bool validExpression(dsiConstraintExpressionType constraintExpType, str exp, StrBuf error)
New: extend public bool validExpression(dsiConstraintExpressionType constraintExpType, str exp, str vendor, StrBuf error)

Previously deprecated Catalog Creator Constraints Tab classes and interfaces are now removed. New classes and interfaces are found in the package.

// dcDBuilderConstraintsCard.cm
Removed: public dcBuildEditConstraintsCard(DcDBBuilderCardWindow cw, int index=700)
Removed: public DcDBBuilderEditConstraintsCard dcDBBuilderEditConstraintsCard(Window this=null)
Removed: public class DcDBBuilderEditConstraintsCard 

// dcDBuilderConstraintsCells.cm
Removed: public class DcConstraintColumn
Removed: public class DcConstraintGridCell
Removed: public class DcConstraintExpColumn
Removed: public class DcConstraintExpGridCell 
Removed: public class DcConstraintCodeColumn 
Removed: public class DcConstraintCodeGridCell 
Removed: public class DcConstraintExpTypeColumn
Removed: public class DcConstraintExpTypeGridCell

// dcDBuilderConstraintsGridWindow.cm
Removed: public DcDBBuilderConstraintsSS dcConstraintsGridWindow
Removed: public class DcDBBuilderConstraintsSS

// dcDBuilderConstraintsShrink.cm
Removed: public class DcFramelessExpressionShrink

custom.dataCatalog.builder.variantsTables

Removed previously deprecated field for Variants Table UI class for Catalog Creator

// DcVariantsTableGridSubWindow (dcVariantsTableGridSubWindow.cm)
Removed: public DsButton importBtn

cm.abstract.dataSymbol

ItemTag key

ItemTags generated from DsPart will now use the data's dataId (if set) for the ItemTag key. If this isn't set, it will fallback to the previous behavior of using the styleNr and then the articleCode.

Cached Parts

The cached parts field on DsPData is now copy=null to avoid undo crashes.

cm.abstract.k2

Cabinet insert previews cache

Previews for cabinet inserts are now cached on a fuzzy basis: A cabinet with similar shape to one already cached will reuse the preview. Two cabinets count as similar if their shapes have the same previewCacheKey(). Note that the user can always clear their cache through the "Clear cached images" button in the Essential Kitchen EU library.

K2AlternativeValidateFunction

Due to a bug, alternatives would spread through any snappers it existed on before the update (regardless of their auto adoption flags), if it had at least one snapper next to it that was auto adopting. It now respects snappers auto adoption state as it should.

This will also cause alternatives to not spread to snappers unless they've been invalidated. Verify that your alternatives (custom implemented moldings/worktops/soffits for example) updates as they should when invalidated through e.g. a #connect event.

If your code relied on this bug to force snappers to update, you might need to make sure that more of your snappers than before are invalidated. Call <snapper>.invalidateBehaviorsIfRequired(...) with #connect or #propagatedConnect as the argument to invalidate them. See cm/abstract/k2/molding/k2AutoMoldingBehavior.cm for an example of this.

K2WtBuiltInPlacementBehavior

K2WtBuiltInPlacementBehavior used to automatically add K2AutoWorktopBuiltInPosBehavior to snappers spawned by its spawner. This no longer happens automatically.

In K2Creator, please add a basic K2AutoWorktopBuiltInPosBehavior behavior to each spawner that uses K2WtBuiltInPlacementBehavior. This is needed for its graphics to update properly as alignment is changed.

K2CategoryBehavior changes

K2CategoryBehavior is now added to all snappers by default from K2SnapperSpawner.gatherBehaviors(). If it has been manually added in K2Creator it needs to be removed from the catalog.

Multiple assortments

For appliances we now support selecting inserts from multiple assortments. To utilize this feature the properties for the insert need to be on the root insert snapper and the insert needs to manage the connection to the catalog for those properties. To make the front responsible for its properties it needs a behavior that has set to true.

Removed reduntant updateSymByRebuild

Removed updateSymByRebuild from K2CabinetGfxBehavior::userPropertyChanged. Should no longer be needed since updateSym gets called after every shape prop change.

XpLengthPartQuantity

XpLengthPartQuantity has been renamed to XpCutRunningMeterPartQuantity and XpRunningMeterPartQuantity has been added.

XpRunningMeterPartQuantity gives a quantity equal to the length of the measure. XpCutRunningMeterPartQuantity gives a quantity equal to the number of articles of a given length that are needed.

cm.abstract.material

Pre-fetch part hook for MaterialLegendController

We have identified several scenarios where material legends using MaterialLegendController were displaying excess materials. The solution is to register a pre-fetch part hook to clear temporary legend materials.

Old:

public FOMatLegendController foMatLegendController("FODataMaterialLegend");


/**
 * Append a SymbolMaterial to the DsMaterialLegend for the specified world.
 */
public void foDataAppendLegendMaterial(Material m, Snapper owner) {
    once {
	    appendPostFinalizeHook(function foDataPostFinalizeLegendHook);
    }

    foMatLegendController.appendLegendMaterial(m, owner, foDataMLCreator);
}

New:

public FOMatLegendController foMatLegendController("FODataMaterialLegend") : keep;


/**
 * Append a SymbolMaterial to the DsMaterialLegend for the specified world.
 */
public void foDataAppendLegendMaterial(Material m, Snapper owner) {
    once {
    	appendPreFetchPartsHook(function foDataPreFetchPartsLegendHook);  // new line
	    appendPostFinalizeHook(function foDataPostFinalizeLegendHook);
    }

    foMatLegendController.appendLegendMaterial(m, owner, foDataMLCreator);
}


/**
 * Pre fetch part hook for clearing temp legend materials.
 */
private void foDataPreFetchPartsLegendHook(Space space) {
    MaterialLegendController controller = foMatLegendController;
    if (controller) controller.clearTempLegendMaterials();
}

For a more generic code you can refer to cm/abstract/material/hooks.cm

Using OfficeMaterialLeg with MaterialLegendController

MaterialLegendController cannot be constructed using "OfficeMaterialLeg" as its auxillary key. If you are affected, you can migrate your old legends by extending MaterialLegendController and override previouslyUsingLegacyMLKey() to return true.

Old:

public MaterialLegendController foMatLegendController("OfficeMaterialLeg") : keep;

New:

public FOMatLegendController foMatLegendController("FOMaterialLegend") : keep;


/**
 * FO MaterialLegendController.
 */
public class FOMatLegendController extends MaterialLegendController {

    /**
     * Previously using OfficeMaterialLeg?
     */
    public bool previouslyUsingLegacyMLKey() {
	    return true;
    }
}

cm.abstract.materialHandling

public class MhStorageSpawnerSelector extends MhSystemSpawnerSelector
    public SnapperSpawner spawner(LayerSet classification, Object env=null) {
        Bugfix: Fixed spawner selection to prioritize exact match, over an equally-attractive-but-partial match.
        There were cases where an exact-match come later due to us iterating in alphabetical order (through allSorted)
        and we would return only a partial match.


MhStorageEditorDialog
- Issues with having leftover `Windows` leaking UI controls in `MhStorageEditorDialog` and `MhStorageConfigurator` has been fixed. You may need to double check your UI behaviors if you do any update or removal of UI controls dynamically.
- `buildPropertiesSubWindow()` for building `MhStorageEditorItem` now use `item.class.toS` instead of `item.label` as key for the `Card`.


MhSnapperShape
- Note: There are two versions of the localBound method which can be easily confused: localBound() and localBound(symbol[])
- localBound() now checks for owner before executing the localBoundWithChildren() case, and will return localBound(null) which returns the geometry's bound in other cases. This fixes the inconsistent bound returned during contexts in which the shape have not obtained an owner yet.


MhLevelInsertHeightBehavior changes

The freeSpaceAbove property is no longer visible when there is no level above the currently selected snapper.

Old:
    /**
     * Show height prop.
     */
    extend public void showHeightProperties() {
        for (k, _ in getPropsWithAttribute(#height))
          if (corePropertyHidden(k)) showCoreProperty(k);
    }


New:
    /**
     * Show height prop.
     */
    extend public void showHeightProperties(MhSnapper owner, MhSnapper toSnapper) {
        for (k, _ in getPropsWithAttribute(#height))
          if (corePropertyHidden(k) and propIsVisible(k, owner, toSnapper)) showCoreProperty(k);
    }


    /**
     * Property is visible?
     */
    extend public bool propIsVisible(str key, MhSnapper owner, MhSnapper toSnapper) {
        if (key == "freeSpaceAbove") {
            <MhSnapper firstAbove, MhSnapper firstBelow> = getFirstAboveAndBelowSnappers(owner, toSnapper, owner.pos.z);
            if (!firstAbove) return false;
        }
        return true;
    }

These methods have been introduced to allow more control over the bounds used to calculate freeSpaceAbove and freeSpaceBelow. Override geoSymbolsForCalculation() to control what geometries should be included in the bound.

    /**
     * Free spaces domains.
     * v0 = freeSpaceAbove domain.
     * v1 = freeSpaceBelow domain.
     */
    extend public <SubSet, SubSet> freeSpacesDomains(MhSnapper firstAbove, MhSnapper firstBelow, MhSnapper owner, MhSnapper toSnapper, Object env=null) {
        return <coreProperty("freeSpaceAbove").?domain, coreProperty("freeSpaceBelow").?domain>;
    }


    /**
     * Get snapper bound used for calculation.
     */
    extend public box boundForCalculation(MhSnapper snapper, bool toSpace=false) {
        Box res = cachedBounds.get(snapper);
        if (!res) {
            res = shapeBoundWithChildren(snapper, geoSymbolsForCalculation());
            cachedBounds.put(snapper, res);
        }
        if (toSpace) return res.v.transformed(snapper.toSpaceTransform);
        return res.v;
    }


    /**
     * Symbols used to retrieve geometries for snapper bound used for calculation.
     */
    extend public symbol[] geoSymbolsForCalculation() {
        return [sLevel, sUnitLoad, sTunnel];
    }

calculateFreeSpaceAboveAndBelow() has been updated to use boundForCalculation().

Old:
    /**
     * Calculate free space above and below.
     */
    extend public <Double, Double> calculateFreeSpaceAboveAndBelow(Snapper owner, Snapper toSnapper,
                                                                   double insertH,
                                                                   Snapper specificAboveSnapper=null,
                                                                   Snapper specificBelowSnapper=null) {
        box b = owner.localBound();
        box b0 = toSnapper.localBound;
        Double above = b0.h - insertH;
        Double below = insertH b.p0.z;

        <Snapper firstAbove, Snapper firstBelow> = getFirstAboveAndBelowSnappers(owner, toSnapper, insertH);

        if (firstAbove) {
            above = firstAbove.pos.z - insertH - b.h;
        }

        if (firstBelow) {
            below = insertH - firstBelow.pos.z - firstBelow.localBound.h;
        }

        if (specificAboveSnapper and specificAboveSnapper != firstAbove) above = null;
        if (specificBelowSnapper and specificBelowSnapper != firstBelow) below = null;

        return <above, below>;
    }


New:
    /**
     * Calculate free space above and below.
     */
    extend public <Double, Double> calculateFreeSpaceAboveAndBelow(MhSnapper owner, MhSnapper toSnapper,
                                                                   double insertH,
                                                                   Snapper specificAboveSnapper=null,
                                                                   Snapper specificBelowSnapper=null) {
        box ownerB = boundForCalculation(owner);
        box b0 = toSnapper.localBound;
        Double above = b0.h - insertH;
        Double below = insertH ownerB.p0.z;

        <MhSnapper firstAbove, MhSnapper firstBelow> = getFirstAboveAndBelowSnappers(owner, toSnapper, insertH);

        if (firstAbove) {
            above = boundForCalculation(firstAbove, toSpace=true).p0.z - insertH - ownerB.p1.z;
        }

        if (firstBelow) {
            below = insertH ownerB.p0.z - boundForCalculation(firstBelow, toSpace=true).p1.z;
        }

        if (specificAboveSnapper and specificAboveSnapper != firstAbove) above = null;
        if (specificBelowSnapper and specificBelowSnapper != firstBelow) below = null;

        return <above, below>;
    }

The updateHeightProperties() method has been broken up into multiple methods to make it easier to override. These methods have also had behavioral changes.

  1. The insertHeight property is unlocked if the locked height is no longer valid (e.g. switching candidates to a different bay with a different height domain).
Old:
    if (heightProp.?locked and heightProp == currentProp) {
        p.z = heightProp.value.safeDouble;
        aboveProp.?unlock();
        belowProp.?unlock();
    }


New:
    /**
     * Check insert height locked.
     */
    extend public void checkInsertHeightLocked(point& p, SubSet heightDomain) {
        CoreProperty heightProp = coreProperty("insertHeight");
        if (heightProp.?locked) { // Height is locked, maintain current height.
            if (heightProp.value !in heightDomain) {
                heightProp.unlock();
            } else {
                p.z = heightProp.value.safeDouble;
                coreProperty("freeSpaceAbove").?unlock();
                coreProperty("freeSpaceBelow").?unlock();
            }
        }
    }
  1. The freeSpaceAbove property is unlocked if the locked value is no longer valid or if there is no longer a snapper above the selected snapper. The z-position calculation has also been modified to utilize the boundForCalculation() method.
Old:
    if (aboveProp.?locked and aboveProp == currentProp) {
        p.z = (firstAbove ? firstAbove.pos.z - b.h : maxH) - aboveProp.value.safeDouble;
        heightProp.?unlock();
        belowProp.?unlock();
    }


New:
    /**
     * Check free space above locked.
     */
    extend public void checkFreeSpaceAboveLocked(point& p, SubSet aboveDomain, MhSnapper firstAbove, MhSnapper owner, MhSnapper toSnapper) {
        CoreProperty aboveProp = coreProperty("freeSpaceAbove");
        if (aboveProp.?locked) {
            if (aboveProp.value !in aboveDomain or !firstAbove) {
                aboveProp.unlock();
            } else {
                p.z = (firstAbove ? firstAbove.pos.z boundForCalculation(firstAbove).p0.z - boundForCalculation(owner).p1.z : toSnapper.localBound.h) - aboveProp.value.safeDouble;
                coreProperty("insertHeight").?unlock();
                coreProperty("freeSpaceBelow").?unlock();
            }
        }
    }
  1. The freeSpaceBelow property is unlocked if the locked value is no longer valid. The z-position calculation has also been modified to utilize the boundForCalculation() method.
Old:
    if (belowProp.?locked and belowProp == currentProp) {
        p.z = firstBelow.MhSnapper.?localBound().p1.z
          (firstBelow? firstBelow.pos.z : 0)
          belowProp.value.safeDouble
          - b.p0.z;

        heightProp.?unlock();
        aboveProp.?unlock();
    }


New:
    /**
     * Check free space below locked.
     */
    extend public void checkFreeSpaceBelowLocked(point& p, SubSet belowDomain, MhSnapper firstBelow, MhSnapper owner, MhSnapper toSnapper) {
        CoreProperty belowProp = coreProperty("freeSpaceBelow");
        if (belowProp.?locked) {
            if (belowProp.value !in belowDomain) {
                belowProp.unlock();
            } else {
                p.z = (firstBelow ? boundForCalculation(firstBelow, toSpace=true).p1.z : 0) belowProp.value.safeDouble - boundForCalculation(owner).p0.z;
                coreProperty("insertHeight").?unlock();
                coreProperty("freeSpaceAbove").?unlock();
            }
        }
    }
  1. The freeSpaceAbove and freeSpaceBelow properties will now be refreshed if their values change but insertHeight did not.
Old:
    double oldInsertHeight = insertHeight;
    if (owner.pos != p) owner.setPos(p);
    insertHeight = p.z;

    <Double above, Double below> = calculateFreeSpaceAboveAndBelow(owner, toSnapper, insertHeight);

    freeSpaceAbove = above.?v;
    freeSpaceBelow = below.?v;

    if (oldInsertHeight != insertHeight) {
        heightProp.?refreshProperty();
        aboveProp.?refreshProperty();
        belowProp.?refreshProperty();

        // Calling diff rebuild instead causes the prop UI to constantly flicker.
        if (domainsUpdated) {
            for (prop in [heightProp, aboveProp, belowProp]) {
                for (CoreDistanceField ctrl in prop.controls) {
                    ctrl.validSubSet = prop.domain;
                    break;
                }
            }
        }
    }


New:
    /**
     * Update height and space values.
     */
    extend public void updateHeightAndSpaceValues(point p, MhSnapper owner, MhSnapper toSnapper, bool domainsUpdated) {
        double oldInsertHeight = insertHeight;
        if (owner.pos != p) owner.setPos(p);
        insertHeight = p.z;

        <Double above, Double below> = calculateFreeSpaceAboveAndBelow(owner, toSnapper, insertHeight);

        double oldFreeSpaceAbove = freeSpaceAbove;
        double oldFreeSpaceBelow = freeSpaceBelow;
        freeSpaceAbove = above.?v;
        freeSpaceBelow = below.?v;

        CoreProperty heightProp = coreProperty("insertHeight");
        CoreProperty aboveProp = coreProperty("freeSpaceAbove");
        CoreProperty belowProp = coreProperty("freeSpaceBelow");
        if (oldFreeSpaceAbove != freeSpaceAbove) aboveProp.?refreshProperty();
        if (oldFreeSpaceBelow != freeSpaceBelow) belowProp.?refreshProperty();
        if (oldInsertHeight != insertHeight) heightProp.?refreshProperty();
        // Calling diff rebuild instead causes the prop UI to constantly flicker.
        if (domainsUpdated) {
            for (prop in [heightProp, aboveProp, belowProp]) {
                for (CoreDistanceField ctrl in prop.controls) {
                    ctrl.validSubSet = prop.domain;
                    break;
                }
            }
        }
    }

MhBracingPattern changes

MhBracingPattern now inherits from MhStorageSingleton and should be treated as singletons.

The following functions have been added to retrieve the generic bracing patterns available in the abstract.

/**
 * Bracing pattern singletons.
 */
public MhDBracePattern mhDBracePattern() {
    static MhDBracePattern pattern;
    if (!pattern) ?pattern = mhStorageSingleton(MhDBracePattern());
    return pattern;
}
public MhKBracePattern mhKBracePattern() {
    static MhKBracePattern pattern;
    if (!pattern) ?pattern = mhStorageSingleton(MhKBracePattern());
    return pattern;
}
public MhXBracePattern mhXBracePattern() {
    static MhXBracePattern pattern;
    if (!pattern) ?pattern = mhStorageSingleton(MhXBracePattern());
    return pattern;
}
public MhZBracePattern mhZBracePattern() {
    static MhZBracePattern pattern;
    if (!pattern) ?pattern = mhStorageSingleton(MhZBracePattern());
    return pattern;
}

MhBayLevelEditorItem changes

applySnappers() previously only returned the main snapper of the editor space selection. It has been changed to now return all level snappers and unit load snappers in the selection.

Old:
    /**
     * Apply snappers.
     */
    public MhSnapper{} applySnappers(Space space) {
        if (SpaceSelection selection = space.selection) {
            ?MhSnapper main = selection.main;
            if (main.isLevel) return {MhSnapper: main};
        }

        return super(..);
    }


New:
    /**
     * Apply snappers.
     */
    public MhSnapper{} applySnappers(Space space) {
        if (SpaceSelection selection = space.selection) {
            MhSnapper{} res();
            for (MhSnapper snapper in selection.snappers) {
                if (snapper.isLevel or snapper.isUnitLoad) {
                    res << snapper;
                }
            }
            return res;
        }

        return super(..);
    }

Pre configurator preview number of aisles

Previously only 1 aisle would be generated for the pre configurator preview snappers. It is now possible to override this to generate different number of aisles based on the selected row layout. We now also generate 2 aisles for when the selected row layout is of class MhRowSingleEndLayout.

Old:
    /**
     * Setup row animation info for preview population.
     */
    extend public MhRowAnimationInfo rowAnimationInfo(MhStorageConfiguration config, MhSnapper row) {
        ...
        info.crossAisleDist = config.calcWidthFromAisles(info, 1);
        return info;
    }


New:
    /**
     * Setup row animation info for preview population.
     */
    extend public MhRowAnimationInfo rowAnimationInfo(MhStorageConfiguration config, MhSnapper row) {
        ...
        info.crossAisleDist = config.calcWidthFromAisles(info, rowNumAisles(info.rowLayout));
        return info;
    }


    /**
     * Row number of aisles for preview population.
     */
    extend public int rowNumAisles(MhRowLayout layout) {
        if (layout in MhRowSingleEndLayout) return 2;
        return 1;
    }

There are now new MhStorageConfigurationPreview classes to handle number of aisles for the MhRowSingleEndDoubleDeepLayout or MhCantileverSingleEndRowLayout layouts.

New:
/**
 * Racking configuration preview.
 */
public class MhRackingConfigurationPreview extends MhStorageConfigurationPreview {

    /**
     * Row number of aisles for preview population.
     */
    public int rowNumAisles(MhRowLayout layout) {
        if (layout in MhRowSingleEndDoubleDeepLayout) return 2;
        return super(..);
    }
}


/**
 * Cantilever configuration preview.
 */
public class MhCantileverConfigurationPreview extends MhRackingConfigurationPreview {

    /**
     * Row number of aisles for preview population.
     */
    public int rowNumAisles(MhRowLayout layout) {
        if (layout in MhCantileverSingleEndRowLayout) return 2;
        return super(..);
    }
}


Use:
public class MhRackingConfiguration extends MhStorageRowConfiguration {
    /**
     * Preview behavior.
     */
    public MhStorageConfigurationPreview configurationPreview() {
        if (!preview) preview = MhRackingConfigurationPreview();
        return preview;
    }
}

cm.abstract.materialHandling.storage.racking.cantilever

Abstract cantilever system populate changes

uprightDepth is now meant to be included in frameDepth by default, so we no longer manually add that value when retrieving the total frame depth.

Old:
public class MhCantileverSinglePopulator extends MhCantileverRowPopulator {
    public double cantileverFrameDepth() {
        double uprightD = uprightDepth;
        double frameD = frameDepth;
        return frameD uprightD;
    }
}


New:
public class MhCantileverSinglePopulator extends MhCantileverRowPopulator {
    public double cantileverFrameDepth() {
        return frameDepth;
    }
}


Old:
public class MhCantileverDoublePopulator extends MhCantileverRowPopulator {
    public double cantileverFrameDepth() {
        double uprightD = uprightDepth;
        double frameD = frameDepth;
        return frameD * 2 uprightD;
    }
}


New:
public class MhCantileverDoublePopulator extends MhCantileverRowPopulator {
    public double cantileverFrameDepth() {
        return frameDepth*2 - uprightDepth;
    }
}


Old:
public class MhCantileverRowPopulator extends MhStorageRowPopulator : abstract {
    extend public double cantileverBayDepth() {
        return frameDepth;
    }
}


New:
public class MhCantileverRowPopulator extends MhStorageRowPopulator : abstract {
    extend public double cantileverBayDepth() {
        return frameDepth - uprightDepth;
    }
}

The center bay has a new symbol added to it's classification sBayCenter.

Old:
    public MhEngineEntry createRackingRowEntry() {
        ...
        LayerSet centerBayLayer = layerSet(sBay, sFlueGap);
    }


New:
    public MhEngineEntry createRackingRowEntry() {
        ...
        LayerSet centerBayLayer = layerSet(sBay, sFlueGap, sBayCenter);
    }

Cantilever arm population on frames

Further changes have been made to construct and populate arm snappers on frame snappers.

public class MhCantileverLevelConstructionalPopulateFunction extends MhLevelConstructionalPopulateFunction {

    /**
     * Populator
     */
    public MhPopulator populator(box populateBox) {
        MhPopulator res = super(..);
        if (res as MhStepperPopulator) {
            point p = populateBox.p0;
            p.x += populateBox.w/2;
            res.startPos = p;
            res.currentPos = p;
        }

        return res;
    }


    /**
     * Execute.
     */
    public Object exec() {
        ?MhStorageConfiguration config = configuration();
        if (!config) return false;

        if (isFrame(parent)) {
            double uprightD = config.frameUprightDepth().safeDouble();
            if (isDouble(parent)) { // Populate arms front and back.
                MhEngineConstructionEntry[] children;
                str key = parent.cacheKey();
                ?children = mhSystemEngineCache.get(key).?obj.copy();

                if (children.empty) {
                    box b = parent.localBound;
                    b.d = (b.d - uprightD)/2;
                    box b1 = b.moved((b.p0.x, b.p1.y uprightD, b.p0.z));
                    MhEngineConstructionEntry[] front = populateLevels(0, b);
                    MhEngineConstructionEntry[] back = populateLevels(0, b1, 0deg);
                    children = front back;
                    mhSystemEngineCache.put(key, children.copy());
                }

                for (c in children) parent << c;

                return true;
            }

            // Single frame.
            box b = parent.localBound();
            b.d = b.d - uprightD;
            addLevels(0, b);
            return true;
        }

        return super(..);
    }


    /**
     * EntryLayout.
     * Why call it a bayEntry when it can be any entry?
     */
    public MhBayEntryLayout entryLayout() {
        if (MhSystemSpawnerSelector ss = getSpawnerSelector) {
            ?MhSnapperSpawner frame = getSpawnerSelector.?spawner(layerSet(sFrame));
            return frame.?entryLayout(ss).?MhBayEntryLayout;
        }

        return null;
    }


    /**
     * Default entry.
     */
    public MhEngineConstructionEntry defaultLevelEntry(box b, LayerSet classification) {
        if (isFrame(parent)) return super(..);
        return null;
    }
public class MhCantileverFrameEngineBehavior extends MhFrameEngineBehavior {

    /**
     * Put engine function to run.
     */
    public void putEngineRunFunctions(MhSnapper snapper, symbol event="", Object env=null) {
        if (!snapper) return;
        MhEngine engine = engine(snapper);
        switch(event) {
          case sShapeChange, sShapeAndSymChange : {
              if (env as MhSnapperChangedEnv) {
                  bool stealing = false;

                  if (snapper != env.owner) {
                      if (MhSnapper stealSource = getStealSource(env, snapper)) {
                          stealing = true;
                          mhPutEngineRunFunction(engine, "rowChildStealConfig", snapper=snapper, source=stealSource);
                      }
                  }

                  Object holeZDomain = snapper.domain("holeZDomain");
                  CollisionPrimitive additionalPrim = additionalPrim(snapper);
                  if (env.translatedKey in ["uprightH", "h"] and !stealing) {
                      if (levelPopulate(snapper)) {
                          // level height changed
                          mhPutEngineRunFunction(engine, "levelPopulate", snapper=snapper,
                                                 holeZDomain=holeZDomain,
                                                 loadWithinLimit=loadWithinLimit(snapper),
                                                 revertAutoULPopulate=revertAutoULPopulate(snapper),
                                                 additionalPrim=additionalPrim,
                                                 configKey=snapper.configName);
                      }
                  }
                  if (env.translatedKey in ["d", "h"] and !stealing) {
                      // Run level arrange, since cantilever levels are children to frames.
                      mhPutEngineRunFunction(engine, "levelArrange", snapper=snapper,
                                             holeZDomain=holeZDomain,
                                             additionalPrim=additionalPrim);
                  }
              }
          }
          case sSnapperInserted: {
              Object holeZDomain = snapper.domain("holeZDomain");
              CollisionPrimitive additionalPrim = additionalPrim(snapper);

              // Run level arrange, since cantilever levels are children to frames.
              mhPutEngineRunFunction(engine, "levelArrange", snapper=snapper,
                                     holeZDomain=holeZDomain,
                                     additionalPrim=additionalPrim);

          }
          default : {
          }
        };
        super(..);
    }


    /**
     * Collect engine function to run.
     */
    public void fetchEngineFunctionsRunG2(MhEngineFunctionRun[] functions, MhSnapper snapper, symbol event="", Object env=null) {
        super(..);
        switch (event) {
          case sShapeChange, sShapeAndSymChange : {
              functions << MhEngineFunctionRun("rowChildStealConfig");
              functions << MhEngineFunctionRun("levelPopulate");
              functions << MhEngineFunctionRun("levelArrange");
          }
          case sSnapperInserted: {
              functions << MhEngineFunctionRun("levelArrange");
          }
          default :;
        };
    }


    /**
     * Gather function args.
     */
    public str->Object engineFunctionArgsG2(MhSnapper snapper, MhEngineFunctionRun func, symbol event="",
                                                   MhPreprocessArgsEnv preprocessArgs=null, Object env=null) {
        switch (func.name) {
          case "levelPopulate" : {
              return props { snapper=snapper,
                    holeZDomain=snapper.domain("holeZDomain"),
                    loadWithinLimit=loadWithinLimit(snapper),
                    revertAutoULPopulate=revertAutoULPopulate(snapper),
                    additionalPrim=additionalPrim(snapper),
                    configKey=snapper.configName };
          }

          case "levelArrange" : {
              return props { snapper=snapper,
                    holeZDomain=snapper.domain("holeZDomain"),
                    additionalPrim=additionalPrim(snapper) };
          }

          default :;
        };


        return super(..);
    }


    /**
     * Accepts function run.
     */
    public bool acceptFunctionRunG2(MhSnapper snapper, MhEngineFunctionRun func, symbol event="", Object env=null) {
        switch (func.name) {
          case "levelPopulate" : {
              if (env as MhSnapperChangedEnv) {
                  if (snapper != env.owner) {
                      if (MhSnapper stealSource = getStealSource(env, snapper)) {
                          return false;
                      }
                  }

                  return env.translatedKey in ["uprightH", "h"] and levelPopulate(snapper);
              }
              return false;
          }
          case "levelArrange" : {
              if (event in [sShapeChange, sShapeAndSymChange]) {
                  if (env as MhSnapperChangedEnv) {
                      if (snapper != env.owner) {
                          if (MhSnapper stealSource = getStealSource(env, snapper)) {
                              return false;
                          }
                      }
                      return env.translatedKey in ["d", "h"];
                  }
                  return false;
              } else if (event == sSnapperInserted) {
                  return true;
              }
          }
          default :;
        };
        return super(..);
    }


    /**
     * Invalidate and return true if required.
     */
    public bool invalidateIfRequired(Object owner, symbol k, MhSnapperChangedEnv env=null) {
        if (k == sAfterInitialExport) return true;
        return super(..);
    }

cm.abstract.office

Office Snapper Manager

OfficeSnapperManagerRedView3D now will call getDialog().updateCoreProperties() when the selection is changed in the view.

Made fixes/improvements to the dialog and OfficeSnapperManager in regards to its support for G2 quick properties.

Panel Skin Animations

Updated how last values are handled for PanelSkinInsertAnimation.

Fix a graphical issue with the PanelSkinArrowVessel where it wasn't properly rotated

Panels/Junctions

beginSideStretch(Connector c) now calls pickedUp() after stretchDisconnectInGroup().

New SIF abstract

If you have used SIF rs in your extension and they are now missing, you have the following options:

  1. Move all SIF functionality to its own pacakge, then specify next rs package in your extension's init.
    putNextRsPkg("custom.yourExtension.sif", "cm.abstract.sif")
    
  2. Explicitly call getRs specifying the cm.abstract.sif package.
    Old: $sifFilesFilter;
    New: getRs("sifFilesFilter", "cm.abstract.sif");
    

Lazy startup of cm.abstract.office and removal of implicit part columns registration

cm.abstract.office will no longer always startup for users that do not require it, as part of core package dependency cleanup and performance optimization. Previously, starting cm.abstract.dataSymbol will always result in the startup of cm.abstract.office.

This may result in some PartColumns from cm.abstract.part not being registered automatically at all times as before, manifesting in the form of a crash for extensions that do not directly depend on cm.abstract.office.

Suggested fix is to explicitly declare and register the part columns you need in your custom extensions by calling registerPartColumn(). You may get warnings if the column have already been registered by other extensions, but it should able to be ignored safely.

AOComponent

AOComponent now has it's propdefs set as cached=true.

Fixed a bug where you couldn't set an AOComponent's parent to null via the propdef.

Typical Blocks

Fixed a bug where spanned stack frames could cause panels to go below the floor due to snapping lower then expected.

cm.core

cm.core

COM Cache Changes

In an effort to lessen drawing file size, CustomerOwnMaterials are no longer stored in World.auxiliaryData and have been moved to World.cachedData.

Note: This means unused COMs are no longer saved with the drawing.

For direct access to a COM, we advise usage of cm.abstract.material.customerOwnMaterials(World world)

Example:

if (CustomerOwnMaterial com = customerOwnMaterials(mainWorld()).?find(myKey)) {
   // do something with your COM
}

Compontent

Updated the base constructor to call initialize.

Old:
public constructor() { /** Default */ }

New:
public constructor() {
    super(null);
    initialize();
}

Property Stretch

Added a bool to allow for code to check whether the animation is in the doSuperStretch() method.

Added a call to droped() when it inserts snappers from super stretch

View mode changes

setDefaultViewModes() now acts on mainWorld() instead of activeWorld(), so it is no longer dependent on the last view that was made active.

Old:
/**
 * Set default view modes.
 */
public void setDefaultViewModes() {
    if (World world = activeWorld()) {
        if ((ViewMode[]) viewModes = defaultViewModes()) {
            world.setViewModes(viewModes);
        }
    }
}


New:
/**
 * Set default view modes.
 */
public void setDefaultViewModes() {
    if (World world = mainWorld(selectWorldIfNull=false)) {
        if ((ViewMode[]) viewModes = defaultViewModes()) {
            world.setViewModes(viewModes);
        }
    }
}

calculation.cm

Added new fields to calculateWorldPrice for BOM analytics purpose.

Old: public double calculateWorldPrice(Space space, bool ignoreGlobalAdjustments=false, bool abortable=false, Int partCount=null) {
New: public double calculateWorldPrice(Space space, bool ignoreGlobalAdjustments=false, bool abortable=false, Int partCount=null,
									   Timespan timelimit=null, Datetime deadline=null, bool flushSomePrices=false) {

cm.subset.doubleRange.cm

An issue was fixed in the DoubleRange and DoubleSubRange classes which could potientially cause an infinite loop.

old:
    public Object next(Object o) {
        (...)
	    if (res > maxV) return maxV;
        (...)
    }
New:
    public Object next(Object o) {
        (...)
	    if (res > maxV) return null;
        (...)
    }
Old:
    public Object previous(Object o)  {
        (...)
	    if (res < minV) return minV;
        (...)
    }
New:
    public Object previous(Object o)  {
        (...)
	    if (res < minV) return null;
        (...)
    }

cm.subset.gProp.cm

Added: public class ToolTipGProp extends GProp {}

cm.core.block

Change how block stores lights in FetchLightEnv3D's entries.

Before 14.5 the collection of lights in a block space was stored incorretly in the FetchLightEnv3D. It tryed to store etch individual light per snapper in the entries map. This hade the side effect that etch light over write the privies light so only on light would be stored per snapper. This has now been change to store a Light3D[] instead for etch snapper. This means that any code that uses the FetchLightEnv3D entries field need to change to use a Light3D[] insted of Light3D.

cm.core.photo

cm.std.photo.labView2D.cm

Old:
    public constructor(PhotoLabDialog dialog, Space space, Window parent, View3D peekAtView3D) {
	view2D = REDView2D(space, "lab2DView", parent);
        (...)
New:
    public constructor(PhotoLabDialog dialog, Space space, Window parent, View3D peekAtView3D) {
        view2D = FilteredLabView2D(space, "lab2DView", parent);
	(...)

cm.core.red3D.distributed

zrpcRenderChore.cm

Method processProtocol() has been updated to accurately calculate job transfer progress. If buffer size is used to calculate progress and the number of bytes packed is less than buffer size, progress will not be calculated correctly. Instead we compare the number of bytes packed.

Old:
extend package rpcStatus processProtocol() {
    (...)
    if (zcall.exec() == 0) {
	  if (job) job.packageProgress(packageToSend.first,
	                                packageToSend.last,
	                                factory.bufferSize,     
					factory.totalSize);
    (...)
}
New:
extend package rpcStatus processProtocol() {
    (...)
    if (zcall.exec() == 0) {
	  if (job) job.packageProgress(packageToSend.first,
	                                packageToSend.last,
					factory.bytesPacked.int,
					factory.totalSize);
    (...)
}

cm.core.ui

Calling cm.core.ui.showWorldPropertiesDialog(World, Window) will no longer show the World Properties dialog as that functionality has been moved to the Info Page. Similarly, setting the showWorldPropertiesAtStart boolean in CoreSettings will no longer work to show the dialog.

cm.std.draw3D

Changed subclass of PaperDraw3DPolyReferencePointInsertAnimation

Changed the subclass of PaperDraw3DPolyReferencePointInsertAnimation from Draw3DReferencePointInsertAnimation to Draw3DPolyReferencePointInsertAnimation. This changes the insert animation the "Grouped help points" button in the "Drawing (paper)" library in paperspace from inserting a line to inserting a polyline.

// cm.std.draw3D.draw3DReferencePoint.cm
Modified: public class PaperDraw3DPolyReferencePointInsertAnimation extends Draw3DPolyReferencePointInsertAnimation

cm.std.photo.labBox

cm.std.photo.labFilters.cm

Some of the props added now support tooltips.

Old: 
    public GProp[] initProps() {
	GProp[] seq();
	DoubleRange span(-1, 1);
	seq << GProp("toneMapping", $toneMappingLabel, Bool(true));
	seq << GProp("toneMapBackground", $toneMapBackgroundLabel, Bool(true));
	seq << GProp("opendenoise", $openDenoiseLabel, Bool(true));
        (...)

New:
    public GProp[] initProps() {
	GProp[] seq();
	DoubleRange span(-1, 1);
	seq << TooltipGProp("toneMapping", $toneMappingLabel, Bool(true), $toneMappingTip);
	seq << TooltipGProp("toneMapBackground", $toneMapBackgroundLabel, Bool(true), null, $toneMapBackgroundTip);
	seq << TooltipGProp("opendenoise", $openDenoiseLabel, Bool(true), $openDenoiseTip);
        (...)

cm.win

The methods Window.exit and Window.mouseLeftChild can now get null passed into the to argument.

    extend public void exit(pointI p, Window to) { ... }
    extend public void mouseLeftChild(Window child, Window to) { ... }

This happens when the mouse cursor moves from a CET window and outside of CET. It means that code in these methods needs to be null safe in regards to the to window.