Overview

cm.abstract.k2.library

Empty function removed since ´public bool addSchemeLimb()´ makes sure the function never gets called.

cm.abstract.k2.xp

Global dimension functions removed and replaced with methods in XpShape.

cm.abstract.kitchen

To prepare for a full implementation of AutoElevations in 16.5 Minor, interface changes around AutoMeasure has been done.

cm.abstract.kitchen.design

Parents clientbound changes are no longer passed to parentClientBoundChanged method.

cm.core.calc

articleView.cm

The behaviour of the additional adjustments have been changed to allow for standardization of the main article view. New interfaces have been added and others modified to manipulate adjustments instead.

cm.core.collabG3

The overall stability and performance of CollabPro has been improved:

  • A facelift to simplify user experience
  • Cleanup and simplification of window resizing logic
  • Improvements to logging and error handling
  • CollabPro now attempts to synchronize realtime project data with server on load, before applying appropriate load world or space hooks, to ensure data consistency.
  • RTInvalidate calls that were invoked before the RT runtime is initialized are now deferred and will be invoked when the runtime is ready.

See also related changes in cm.network.cbb.

For a developer's introduction to supporting CollabPro (G3), refer to this document.

cm.core.dwg

The DWG dialog (commonly known as "CAD Settings") have received a large update to allow selecting and editing multiple DWGs at the same time. As a result, many interfaces have been updated to accommodate the new functionality.

cm.core.geometry2D.advanced

Apath2D would sometimes return incorrect or inconsistent results when asking whether it contains a point or not.

It has been updated to have fewer errors and more consistent behavior when the point lies on the edge of it. This may in rare cases cause issues in code that relied on the incorrect behavior. Fixing these issues should lead to CET having fewer unexpected issues overall.

cm.format.dwg

Cleanup of Dwg entity classes, removing redundant constructors. In preparation for upcomming improved DWG export.

cm.network.cbb

CBBClient is the networking client used by CollabPro to exchange data with the cloud. We have improved the connection stability and reliability through better error handling and clearer status information.

cm.win

DIB Images

The handling of DIB (Device Independent Bitmap) images by CET Core has been improved, and it can now draw DibImage that have transparency.

As DIB images are regular memory allocated objects, it does not consume Windows GDI handles, resulting in reduced system resource usage, improving performance and stability.

MessageWindow

MessageWindow was originally made only to show in toolbox libraries. But now that there are more cases of it being used generally for UI, changes are made to easily use and size this outside of toolbox libraries. It sizes itself according to the text along with 3 sizing modes:

  • No width is provided : Size itself without text wrapping. (Use setWidth passing a 0 value)
  • Width is provided : Attempts to wrap the text if it crosses the width provided. (Use setWidth)
  • Calling extendRight : Attempts to wrap text according to how far it can extend right. (Use extendRight)

cm.win.advanced

As CET manufacturers grow in scale regarding products, we also need to keep that in mind of GDI Ojects from Windows having a limited amount for use in showing each image. While using any ExploreDialog in CET, the amount of GDI Objects use can soar if the folder being navigated is full of products being shown. Hence changes are made to use DibImage instead of MemoryImage for that very purpose. It is strongly recommended to look through your code base and determine whether to migrate into using DibImage wherever large amounts of images are being shown. Here are some suggestions on how to migrate.

Read as DibImage from a source.

If the image is loaded from a source, it is best to save the image as a DibImage from the start. Utilizing the functions inside cm/win/DibImage.cm, which takes in either Url, RawImageData, MagickWand, Stream.

MemoryImage as a transient.

If beginning with from DibImage is too difficult, you can instead opt to convert MemoryImage to DibImage, then destroy it, else it depends on the garbage collector to clean up the GDI Object. You can utilize memToDibImg(MemoryImage) in cm/win/DibImage.cm

Compile Time Changes

Compiler and CM Language

Props argument compiler warning

A warning has been added when passing a str->Object with the same name as the props argument name. This to make the developer aware that this will NOT pass your value as argument. Keyword and value will be added to props.

public class MyClass {
    public constructor() { }

    extend public void put(str key, props: args) {
      	pln(#args);
    }
}

{
    str->Object args = {str->Object: "a"->1};

    MyClass myclass();
    myclass.put("key", args=args);// props argument value has the same name as the props argument name
}

args={"args"->{"a"->1}}

cm.abstract

class AbsItemTagInfo

Two new constructors have been added to the AbsItemTagInfo class.

  • str articleCode parameter has been added
  • Allows for setting the new str articleCode field on construction
New: public constructor(str text, str k, AbsDwgLayerInfo info, str articleCode, double textHeight=4inch) {}
New: public constructor(str text, str k, AbsDwgLayerInfo info, str textType, str articleCode) {}

cm.abstract.dataSymInterface

Dib image resizing functions

The following resizing interfaces have been replaced:

Old: public DibImage dsDibResize3(DibImage im, sizeI newSize, str filter=null) : deprecated {
New: public void dibResize(DibImage im, sizeI newSize, str filter=null) {

class DsiPData

Changed: public double basePrice()

The basePrice method was refactored to remove the final and inline modifiers and is now declared as an extend public method.

Old → New behavior

Old:

  • Declared as final → could not be overridden by subclasses.
  • Marked inline → compiler inlined the method for performance optimization.

New:

  • Declared as extend public → method can now be extended/overridden.
  • inline removed → no longer enforced as an inline method.
  • Core logic remains unchanged:
    • Returns the styleProduct price if present.
    • Returns -0.01 otherwise.

cm.abstract.dataSymbol

class DsPart

Constructor Migration: List Price → Base Price + Option Price Sum

The DsPart class constructors have been expanded and split to support the new part pricing system (introduced in 16.5). New constructors now accept basePrice and optionPriceSum separately.

Old constructors that use list price remain available but are marked for future deprecation.

Old Constructors
  • constructor(Snapper, DsPData, double listPrice, …)
  • constructor(Snapper, DsPData, str articleCode, str description, double listPrice, …)
  • These constructors use list price (a single cached price) instead of separating base + options.
  • Still supported in 16.5 for compatibility.
New Constructors (preferred, introduced in 16.5)
  • constructor(Snapper, DsPData, double basePrice, Double optionPriceSum, …)
  • constructor(Snapper, DsPData, str articleCode, str description, double basePrice, Double optionPriceSum, …)
  • These constructors use separate base price and option prices instead of utilizing a single cached list price
  • Preferred in v16.5+
Impact
  • If you use list price constructors today → update to the new basePrice + optionPriceSum pattern
  • See new pricing API documentation in cm.core.part.Part compile-time section for more migration tips

New Pricing API

The following functions were introduced or modified to support the transition to the new part pricing system. Find more documentation on the new pricing system in the compile-time section for cm.core.part.Part.

Added: public Double generateOptionPriceSum() {}
Removed: public double basePrice() {}
Removed: public void performPriceConversions(ProjectInformation projectInfo) {}

Added: extend public Double generateOptionPriceSum()

Old → New behavior

Old:

  • Not applicable.

New:

  • If the Part has an associated pData (DsPData):
    • Delegates to pData.optionPriceSum(specOptions, this)
  • Otherwise:
    • Falls back to superclass implementation (super()).
Impact
  • If you have custom option price logic, override generateOptionPriceSum() in your extended Part class.
  • Invalidate the cache (invalidateOptionPriceSum()) when option state changes to trigger regeneration.

Removed: public double basePrice() {}

The basePrice method was expanded to support the new pricing framework by adding parameters and delegating to the superclass when new pricing is enabled.

Old: public double basePrice() {}
New: public double basePrice(bool includeChildren=false, Space space=null, bool translatePrice=true)
Old → New behavior

Old:

  • Simple signature:
    • public double basePrice()
  • Always derived base price from:
    • Adjustments via PartSpecial (replace or append)
    • _data.basePrice

New:

  • New signature with optional parameters:
    • public double basePrice(bool includeChildren=false, Space space=null, bool translatePrice=true)
  • Behavior changes:
    • If useNewPricing is enabled → delegates directly to super()
    • Otherwise → falls back to legacy logic (_data.basePrice with PartSpecial adjustments)
Impact
  • Functional expansion: method now supports contextual pricing (child parts, space, translation)
  • Under the new pricing system, logic comes from the superclass, not local overrides
  • Legacy behavior retained for backwards compatibility

Removed: public void performPriceConversions(ProjectInformation projectInfo) {}

  • This overidden method was removed as it is a complete duplicate of the super class version (in ProdPart).

New Lead Time Interface

As of 16.5, a new interface for lead time has been added to core Part and overridden in DsPart. It's value is displayed in the new LeadTimeColumn implemented in cm/core/init/leadTimeColumn.cm.

In cm/abstract/dataSymbol/parts.cm

/**
 * Get lead time string.
 * Ex. 20 days, 3 weeks, etc.
 */
public str leadTime() {
	DsProductType product = pData.product();
	if (!product) return super();

	int days;
	for (ref in product.leadTimeProgramRefs()) {
		DsLeadTimeProgramType ltp = pData.leadTimeProgram(ref);
		if (!ltp) continue;
		days = max(days, ltp.days);
	}

	return days > 0 ? spnn(days, ' ', days == 1 ? $day : $days) : "";
}

Other DsPart changes

Removed: public void generateOptAdjustmentSifRows(SpecOption opt, str[] lines) {}

The abstract ProdPart interface for generateOptAdjustmentSifRows has been changed to accept PartOptionItems instead of SpecOptions. As a result, the empty override of this function in DsPart has been removed entirely.

Old: public void generateOptAdjustmentSifRows(SpecOption opt, str[] lines) {}
Changed: public str optSpecialKey(SpecOption opt) {}

The abstract ProdPart interface for optSpecialKey has been changed to accept PartOptionItems instead of SpecOptions.

Old: public str optSpecialKey(SpecOption opt) {}
New: public str optSpecialKey(PartOptionItem  opt) {}
Changed: public PartInfoTree[] infoTrees() {}

When collecting PartInfoTrees on DsParts, the specOptions sequence is no longer filtered by DsSpecOptions. All options within the specOptions sequence will have a tree constructed.

public PartInfoTree[] infoTrees() {
	PartInfoTree[] items();
	PartInfoTree[] parents();
	
Old:	for (DsSpecOption tp in specOptions, index=i) {
New:	for (tp in specOptions, index=i) {
            ...
        }

	return items;
}
Changed: public str flattenableKey() {}

With the addition of the new part attribute description/notes system, DsParts now need to be differentiable/split by their attribute values.

The following change has been made in flattenableKey to account for this:

public str flattenableKey() {
	StrBuf buf(uniqueKey());
	
	...
	
New:	buf << annotationFlattenableKey();

	return buf.retireToS();
}

class DsFreeformPicklistPart

Removed: public double basePrice() {}

The basePrice method was expanded to support the new pricing framework by adding parameters and delegating to the superclass when new pricing is enabled.

Old: public double basePrice() {}
New: public double basePrice(bool includeChildren=false, Space space=null, bool translatePrice=true) {}
Old → New behavior

Old:

  • Simple signature:
    • public double basePrice()
  • Always derived base price from:
    • item.price()
    • Adjustments via PartSpecial (replace or append)

New:

  • New signature with optional parameters:
    • public double basePrice(bool includeChildren=false, Space space=null, bool translatePrice=true)
  • Behavior changes:
    • If useNewPricing is enabled → delegates directly to super()
    • Otherwise → falls back to legacy logic (item.price() with PartSpecial adjustments)

class DsFreeformItem

The following functions were added to support base price for DsFreeformItems.

Added: extend public str currentCurrencyKey(str key) {}
Added: extend public double basePrice() {}
Added: extend public double basePrice=(double value) {}

Added: extend public str currentCurrencyKey(str key) {}

The handling of currency-based pricing was refactored to make currentCurrencyKey more flexible.

Old → New behavior

Old:

  • currentCurrencyKey() directly constructed a key using cDsFPriceKey and the current currency symbol

New:

  • currentCurrencyKey() now delegates to a new overload:
    • currentCurrencyKey(str key) → builds a unique freeform key from the given key and currency symbol.

Added: extend public double basePrice() {} and extend public double basePrice=(double value) {}

Explicit support for base price has been added to DsFreeformItem.

Old → New behavior

Old:

  • Only list price (price) was supported via field data

New:

  • Added explicit basePrice field accessors:
    • basePrice() → retrieves the base price using cDsFBasePriceKey
    • basePrice=(double value) → sets the base price.

cm.abstract.dataSymbol.ui

CatalogTocLimbBuilder changes

We have removed the field info field from CatalogTocLimbBuilder. You can get relevant information from code and versionCode fields.

Old: private DsSqlImportActiveCatsInfo info : public readable;
New: private str code : public readable;
New: private str versionCode : public readable;

DataCatalog changes

The following methods with bool retDibImage=true have been removed. DibImage are now used by default for performance reasons.

Old: extend public Image getThumbImage(Url url, sizeI size=sizeI(),
                                       DsCommunicationCB callback=null,
                                       int priority=2, bool retDibImage=false) {
New: extend public Image getThumbImage(Url url, sizeI size=sizeI(),
                                       DsCommunicationCB callback=null,
                                       int priority=2) {

Old: extend public Image getThumbImage(DsiPData data, sizeI size=sizeI(),
                                       DsCommunicationCB callback=null,
                                       bool useCache=false, int priority=2, bool retDibImage=false) {
New: extend public Image getThumbImage(DsiPData data, sizeI size=sizeI(),
                                       DsCommunicationCB callback=null,
                                       bool useCache=false, int priority=2) {

cm.abstract.dataSymbol.ui.libraryBuilder

dsMemoryImageCache.cm

We have improved handling of DibImage by the system. It can now draw DibImage that have transparency. This means we no longer need convert a DibImage into a MemoryImage and cache it. The following interfaces have been removed:

Removed: public MemoryImage dsThumbnailMemImageCache(DibImage dib) {
Removed: public void startDsMemImageCacheIdle() {
Removed: public int dsMemoryImageCount() {

cm.abstract.draw

class MirrorSelectionAnimation

Old: extend public View currentView() {
New: public WindowView currentView() {

cm.abstract.extrusions

The visibility of many interfaces across the whole abstract have been changed from public to package or private, as they were not intended to be used elsewhere. If you require any of the changed interfaces, consider making a local copy of it or contact developer support.

cm.abstract.k2.library

The following function has been removed from K2ApplianceLibrary:

Removed: public void addSchemeLimb(LibraryLimb parent)

The following function has been removed from K2SinkLibrary:

Removed: public void addSchemeLimb(LibraryLimb parent)

cm.abstract.k2.xp

Global dimension functions removed:

Removed: public double getCabinetWidth(XpShape shape)
Removed: public double getCabinetDepth(XpShape shape)
Removed: public double getMeasureWidth(XpShape shape)
Removed: public double getMeasureDepth(XpShape shape)
Removed: public double getMeasureHeight(XpShape shape)

Use these methods instead:

public double nominalHeight()
public double nominalWidth()
public double nominalHeight()

cm.abstract.kitchen

Dib image for whitegoods appliances

The WgApplianceEnvSnapperDibButton class has been removed as LazySnapper3DMemoryButton has been updated to use DibImage.

Old: public class WgApplianceEnvSnapperDibButton extends SnapperImageButton {
New: public class WgApplianceEnvSnapperButton extends LazySnapper3DMemoryButton {

KitchenAutoMeasureGroup

When adding a measure to the group, the snapper which the measure belongs to needs to be passed along with it.

Old: final public void add(KitchenAutoMeasure measure) {
New: final public void add(Snapper snapper, KitchenAutoMeasure measure) {

createAutoMeasures now returns which snapper that are mapped to which AutoMeasures.

Old: public KitchenAutoMeasure{} createAutoMeasures(Snapper{} snappers) {
New: public Snapper->KitchenAutoMeasure{} createAutoMeasures(Snapper{} snappers) {

accept now requires the AutoMeasure which belongs to the snapper.

Old: final public bool accept(Snapper z) {
New: final public bool accept(Snapper z, KitchenAutoMeasure measure) {

autoMeasureToGroups now requires the AutoMeasures to be mapped from their Snapper.

Old: public KitchenAutoMeasureGroup[] autoMeasuresToGroups(KitchenAutoMeasure{} measures, symbol category=null) {
New: public KitchenAutoMeasureGroup[] autoMeasuresToGroups(Snapper->KitchenAutoMeasure{} measures, symbol category=null) {

KitchenAutoMeasures

The global public function updateAutoMeasures now have closeElevationDialog=false as a default argument.

Old: public void updateAutoMeasures(Space space) {
New: public void updateAutoMeasures(Space space, bool closeElevationDialog=false) 

cm.abstract.kitchen.design

The following function has been removed from DesignDialog:

Removed: public void parentClientBoundChanged()

cm.abstract.material

class COMPart

Removed: public double customListPrice(bool includeChildren=false, str currency=null, Space space=null) {}

The pricing override in COMPart shifted from a custom list price implementation to a standardized basePrice override.

Old: public double customListPrice(bool includeChildren=false, str currency=null, Space space=null) {}
New: public double basePrice(bool includeChildren=false, Space space=null, bool translatePrice=true) {}
Old → New behavior

Old:

  • Method: customListPrice(..)
  • Returned pricePerUnit() as the list price unless includeChildren=true (in which case 0).
  • Treated COM pricing as list-price–based.

New:

  • Method: basePrice(..)
  • Calls super(..) for base price, unless includeChildren=true (still returns 0).
  • Pricing responsibility moved to align with base price in new model instead of custom list price.
Impact
  • Aligns COMPart with the new part pricing API (basePrice + optionPriceSum).
  • Removes dependency on customListPrice (legacy, tied to pre-16.5 system).

cm.abstract.materialHandling

Engine entry changes

In class MhEngineEntryBase, removed argument MhEngine engine from method parent as it is no longer required (explained below).

public class MhEngineEntryBase {
    Old: extend public MhEngineEntry parent(MhEngine engine) { return null; }
    New: extend public MhEngineEntry parent() { return null; }
}

Moved a field MhEngineEntry parent from child class MhEngineConstructionEntry up to the parent class MhEngineSnapperEntry.

public class MhEngineSnapperEntry extends MhEngineBoxEntry {
    New: public MhEngineEntry parent : copy=reference;
}


public class MhEngineConstructionEntry extends MhEngineSnapperEntry {
    Removed: public MhEngineEntry parent : copy=reference;
}

With this, MhEngineSnapperEntry now has a direct reference to its parent entry. The parent() method (previously parent(MhEngine engine)) no longer needs to refer to its snapper's parent.

public class MhEngineSnapperEntry extends MhEngineBoxEntry {
Old:
    /**
     * Parent.
     */
    public MhEngineEntry parent(MhEngine engine) {
        ?MhSnapper parent = z.parent;
        return engine.?entry(parent);
    }


New:
    /**
     * Parent.
     */
    public MhEngineEntry parent() {
        return parent;
    }
}

For your info, there is now a proper interface to change the parent of an engine entry.

public class MhEngineEntryBase {
    New: extend public void changeParent(MhEngine engine, MhEngineEntry newParent) { }

In class MhEngineBoxEntry renamed methods box unmodifedBound() to box unmodifiedBound() and Transform unmodifedTransform() to Transform unmodifiedTransform().

public class MhEngineBoxEntry extends MhEngineCollisionEntry {
    Old: extend public box unmodifedBound() {
    New: extend public box unmodifiedBound() {

    Old: extend public Transform unmodifedTransform() {
    New: extend public Transform unmodifiedTransform() {
}

The public function box unmodifedBoundIncludingChildren(MhEngineBoxEntry entry, MhEngine engine) has been renamed to box unmodifiedBoundIncludingChildren(MhEngineBoxEntry entry, MhEngine engine).

Old: public box unmodifedBoundIncludingChildren(MhEngineBoxEntry entry, MhEngine engine) {
New: public box unmodifiedBoundIncludingChildren(MhEngineBoxEntry entry, MhEngine engine) {

MhAlignLoadsToAisleFunction changes

The method box unmodifedLocalBoundIncludingChildren(MhEngineEntry entry, Transform t, MhEngine engine) has been renamed to box unmodifiedLocalBoundIncludingChildren(MhEngineEntry entry, Transform t, MhEngine engine).

public class MhAlignLoadsToAisleFunction extends MhSystemEngineFunction {
    Old: extend public box unmodifedLocalBoundIncludingChildren(MhEngineEntry entry, Transform t, MhEngine engine) {
    New: extend public box unmodifiedLocalBoundIncludingChildren(MhEngineEntry entry, Transform t, MhEngine engine) {
}

MhMasterLinkFinalizeFunction changes

Added new argument runMultiChildrenLink to this engine function. The purpose of this argument is to link nested bays/frames together (bays within bay, frames within frame). See "Multi-bay and multi-frame support" in New Features for more info.

public class MhMasterLinkFinalizeFunction extends MhSystemEngineFunction {
    /**
     * Run multi children link.
     */
    public Bool runMultiChildrenLink;

MhRowFillVoidEntriesFunction changes

Added new argument flattenedFill to this engine function. The purpose of this argument is for racking systems with multi-bay/multi-frame structure (see "Multi-bay and multi-frame support" in New Features for more info). Set this argument to true if you want to fill empty bay space depth-wise when you have a multi-bay. Control this argument with MhBayRowEngineBehavior.shouldFlattenRowForFill(symbol event="", Object env=null).

public class MhBayRowEngineBehavior extends MhEngineBehavior {
    /**
     * Should flatten row for fill voids?
     */
    extend public bool shouldFlattenRowForFill(symbol event="", Object env=null) {
        return true;
    }
}

We've also broken up the existing logic in fillVoidEntries(MhEngineEntryBlock block) into several new methods for easier override.

public class MhRowFillVoidEntriesFunction extends MhSystemEngineFunction {

    /**
     * Fill removed snappers void entries.
     */
    extend public void fillRemovedSnappersVoidEntries(MhEngineEntryBlock block) {
        ...
    }


    /**
     * Fill gaps between entries with void entries.
     */
    extend public void fillGapsVoidEntries(MhEngineEntryBlock block,
                                           sorted int->MhEngineVoidEntry[] voidEntries) {
        ...
    }

MhRowRemoveLoneFramesFunction changes

Added argument MhEngineEntry{} bayEntries to the method shouldRemoveEntry.

public class MhRowRemoveLoneFramesFunction extends MhSystemEngineFunction {
    Old: extend public bool shouldRemoveEntry(MhEngine engine, MhEngineEntry frameEntry) {
    New: extend public bool shouldRemoveEntry(MhEngine engine, MhEngineEntry frameEntry, MhEngineEntry{} bayEntries) {
}

MhBayConfigurationItem changes

Added argument SnapperFilter filter to the method getBay.

public class MhBayConfigurationItem extends MhStorageConfigurationItem {
    Old: extend public MhSnapper getBay() {
    New: extend public MhSnapper getBay(SnapperFilter filter) {
}

MhRowApplyAnimation changes

Added argument MhSnapperGroup group to the method applyPickedUpSnappers.

public class MhRowApplyAnimation extends MhSnapperApplyAnimation {
    Old: extend public void applyPickedUpSnappers(Snapper[] groupSnappers, Snapper firstS, Snapper first, box oldBound, bool needToRotateChildren) {
    New: extend public void applyPickedUpSnappers(MhSnapperGroup group, Snapper[] groupSnappers, Snapper firstS, Snapper first, box oldBound, bool needToRotateChildren) {
}

Config function changes

Added argument SnapperFilter childFilter to the public function basicConfigEq.

Old: public bool basicConfigEq(MhSnapper a, MhSnapper b) : inline {
New: public bool basicConfigEq(MhSnapper a, MhSnapper b, SnapperFilter childFilter=null) : inline {

Added argument SnapperFilter childFilter to the public function childrenConfigEq.

Old: public bool childrenConfigEq(MhSnapper a, MhSnapper b, function(mhConfigSortEntry, mhConfigSortEntry, Object):int sortFunc, Object env=null) {
New: public bool childrenConfigEq(MhSnapper a, MhSnapper b, function(mhConfigSortEntry, mhConfigSortEntry, Object):int sortFunc, Object env=null, SnapperFilter childFilter=null) {

These work together with MhStorageConfigBehavior now having a new method Snapper childrenFilter() that is used in the existing method configEq(MhSnapper a, MhSnapper b, Object env=null) to now be able to filter out specific children from the equality comparison.

Removed deprecated class MhUnitLoadOffsetBehavior

The class MhUnitLoadOffsetBehavior has been deleted as well as the global singleton mhUnitLoadOffsetBehavior.

As such, we have also updated MhRowAnimationInfo and removed the method Point unitLoadPopulateOffset(MhSnapper snapper) which used this removed class.

public class MhRowAnimationInfo extends CoreObject {

Removed:
    /**
     * Unit load populate offset.
     * Override here if you have different overhang values.
     */
    extend public Point unitLoadPopulateOffset(MhSnapper snapper) {
        for (MhUnitLoadOffsetBehavior b in snapper.behaviors) {
            if (Point p = b.unitLoadPopulateOffset(snapper, configuration())) {
                return p;
            }
        }
        return null;
    }
}

MhBayEditorMeasurementsBehavior changes

Removed method void removeOldMeasurements(Space space) as it is not used.

public class MhBayEditorMeasurementsBehavior extends MhBehavior {

    /**
     * Remove old measurement.
     */
    extend public void removeOldMeasurements(Space space) {
        for (s in space.?snappers)  if (s in MhBayEditorMeasureDimension) space.remove(s);
    }
}

This logic already exists in MhBayEditConfiguration and this class handles the removal and insertion of dimensions (e.g. re-inserting dimensions when pulling props from snappers).

public class MhBayEditConfiguration extends MhStorageEditorConfiguration {

    /**
     * Remove old measurement.
     * Will be added on validate.
     */
    extend public void removeOldMeasurements(Space space) {
        SpaceSelection sel = space.?selection;
        for (MhBayEditorMeasureDimension s in space.?snappers) {
            if (!s.isActive) continue;
            if (s in sel) sel.remove(s);
            space.undoableRemove(s);
        }
    }


    /**
     * Pull props from snappers.
     */
    public void pullPropsFromSnappers() {
        removeOldMeasurements(space);

        for (s in snappers) {
            forChildrenRecursive(MhSnapper c in s) {
                if (c.isBay and hasRealConfig(c)) {
                    pullPropsFromSnapper(c);
                    insertMeasurements(c, space);
                }
            }
        }
    }
}

MhStorageEditorConfiguration changes

Updated interface of method Object{} pullPropsFromSnapper. The argument MhSnapper snapper has been widened to Snapper snapper.

public class MhStorageEditorConfiguration extends MhSystemConfiguration {
    Old: extend public Object{} pullPropsFromSnapper(MhSnapper snapper, Object env=null) {
    New: extend public Object{} pullPropsFromSnapper(Snapper snapper, Object env=null) {
}

MhStorageEditorItem changes

Updated interface of method MhSnapper{} applyToSnappers. The return value MhSnapper{} has been widened to Snapper{}.

public class MhStorageEditorItem extends MhSystemConfigurationItem {
    Old: extend public MhSnapper{} applySnappers(Space space, CoreProperty property=null) {
    New: extend public Snapper{} applySnappers(Space space, CoreProperty property=null) {
}

MhSystemConfiguration changes

The interface of various methods in this class have been updated such that their arguments MhSnapper or MhSnapper{} have been widened to Snapper or Snapper{}. See below for the full list.

public class MhSystemConfiguration extends CoreObject {
    Old: extend public PushPropsVisitor pushPropsVisitor(MhSnapper snapper) {
    New: extend public PushPropsVisitor pushPropsVisitor(Snapper snapper) {

    Old: extend public PushPropsVisitor pullPropsVisitor(MhSnapper snapper) {
    New: extend public PushPropsVisitor pullPropsVisitor(Snapper snapper) {

    Old: extend public void applyTo(MhSnapper{} snappers) {
    New: extend public void applyTo(Snapper{} snappers) {

    Old: extend public void beforeApply(MhSnapper{} snappers, World w=null) {
    New: extend public void beforeApply(Snapper{} snappers, World w=null) {

    Old: extend public void afterApply(MhSnapper{} snappers, MhSnapperChangedEnv[] changedEnvs, World w=null) {
    New: extend public void afterApply(Snapper{} snappers, MhSnapperChangedEnv[] changedEnvs, World w=null) {

    Old: extend public Object{} executeApply(MhSnapper{} snappers) {
    New: extend public Object{} executeApply(Snapper{} snappers) {

    Old: extend public bool allowPush(MhSnapper snapper) {
    New: extend public bool allowPush(Snapper snapper) {
}

MhSystemConfigurationItem changes

Updated interface of method bool pulledFromSnapper. The argument MhSnapper snapper has been widened to Snapper snapper.

public class MhSystemConfigurationItem extends CoreObject {
    Old: extend public bool pulledFromSnapper(MhSnapper snapper) { return false; }
    New: extend public bool pulledFromSnapper(Snapper snapper) { return false; }
}

MhBayRowStretchEngineBehavior changes

The method void putAisleUpdateFunctions(MhEngine engine, MhSnapper snapper, MhSnapper aisleRow, MhSnapper topRow, MhSnapper downRow) has been removed as it is no longer used.

public class MhBayRowStretchEngineBehavior extends MhEngineBehavior {

Removed:
    /**
     * Put connected aisle update function.
     */
    extend public void putAisleUpdateFunctions(MhEngine engine, MhSnapper snapper, MhSnapper aisleRow,
                                                   MhSnapper topRow, MhSnapper downRow) {
        SnapperSet set();
        set <<? topRow;
        set <<? downRow;
        mhPutEngineRunFunction(engine, "aisleUpdateShape", snapper=aisleRow, connectedRows=set);
    }
}

MhFrameConfigBehavior changes

The method bool isFirstFrame(MhSnapper z) has been removed as it is no longer used and moved to custom package.

public class MhFrameConfigBehavior extends MhStorageConfigBehavior {

Removed:
    /**
     * Is first frame in selection?
     */
    extend public bool isFirstFrame(MhSnapper z) {
        Snapper root = z.rootParent;

        SnapperSelection sel = z.singleSelection(null);
        if (sel.count > 1) {
            bool rotated = false;
            forChildren(MhSnapper c in root) if (c.isBay) { rotated  = c.rot != 0deg; break; }
            Snapper[] snappers = sel.snappers.seq;
            snappers.sort(function snapperYPosSort, null);
            if (rotated) snappers.reverse();
            return z == snappers.first;
        }
        return true;
    }
}

Following the removal of the method in abstract, please proceed to implement this within the custom logic for the case of this mething being overriden.

public class DrFrameConfigBehavior extends MhDeepStorageFrameConfigBehavior {
    /**
     * Should put the config to the config manager?
     */
    public bool shouldPutConfigToManager(MhSnapper z) {
            if (!isFirstFrame(z)) return false;
            return true;
    }


    /**
     * Skip config ID?
     */
    public bool skipConfigId(MhSnapper z) {
            if (!isFirstFrame(z)) return true;
            return super(..);
    }


    /**
     * Is first frame in selection?
     */
    extend public bool isFirstFrame(MhSnapper z) {
        ...
    }

MhBayStretchFunction changes

The method void levelPopulate() has been removed as it is no longer used. Replace calls with the following method void levelPopulate(MhSnapper s) instead.

public class MhBayStretchFunction extends MhSystemEngineFunction {
    Removed: extend public void levelPopulate() {
    Replace: extend public void levelPopulate(MhSnapper s) {
}

MhRowFillVoidEntriesFunction changes

The field Rect systemRect has been removed as it is no longer used.

public class MhRowFillVoidEntriesFunction extends MhSystemEngineFunction {
    Removed: public Rect systemRect;
}

MhRowRemoveLoneFramesFunction changes

The method bool shouldRemoveEntry(MhEngine engine, MhEngineEntry frameEntry) has been removed as it is no longer used. A new method bool shouldRemoveEntry(MhEngine engine, MhEngineEntry frameEntry, MhEngineEntry{} bayEntries) has been added so replace calls to this method instead.

public class MhRowRemoveLoneFramesFunction extends MhSystemEngineFunction {
    Removed: extend public bool shouldRemoveEntry(MhEngine engine, MhEngineEntry frameEntry) {
    New: extend public bool shouldRemoveEntry(MhEngine engine, MhEngineEntry frameEntry, MhEngineEntry{} bayEntries) {
}

MhFlueGapAnimation changes

The method void initFrameVessel has been removed as it is no longer used. Replace calls with the following method void initApplyColorFrameVessel() from parent class MhSnapperToolAnimationG2 instead.

public class MhFlueGapAnimation extends MhSnapperSpreadToolAnimation {
    Removed: extend public void initFrameVessel() {
    New: public void initApplyColorFrameVessel() {
}

MhRowApplyAnimation changes

The method void initFrameVessel has been removed as it is no longer used. Replace calls with the following method void initApplyColorFrameVessel() from parent class MhSnapperToolAnimationG2 instead.

public class MhRowApplyAnimation extends MhSnapperApplyAnimation {
    Removed: extend public void initFrameVessel() {
    New: public void initApplyColorFrameVessel() {
}

MhConfigManager changes

The method void validateWaitingList() has been removed as it is no longer used. A new method MhConfigRef[] validateSortedWaitingList() has been added so replace calls to this method instead.

public class MhConfigManager {
    Removed: extend public void validateWaitingList() {
    New: extend public MhConfigRef[] validateSortedWaitingList() {
}

MhSnapperInsertToolAnimation changes

The method MhSnapperInsertedEnv inserted(MhSnapper z, MhSnapperInsertedEnv insertedEnv) has been removed as it is no longer used. Replace calls with the following method MhSnapperInsertedEnv inserted(MhSnapper z, MhSnapperInsertedEnv insertedEnv, SpaceSelection sel) instead.

public class MhSnapperInsertToolAnimation extends MhSnapperSpreadToolAnimation {
    Removed: extend public MhSnapperInsertedEnv inserted(MhSnapper z, MhSnapperInsertedEnv insertedEnv) {
    Replace: extend public MhSnapperInsertedEnv inserted(MhSnapper z, MhSnapperInsertedEnv insertedEnv, SpaceSelection sel) {
}

Example usage:

    SpaceSelection sel = initSelectionForApply();
    for (entry in insertEntries) {
        ... // insert snapppers code
        insertedEnv = inserted(insertSnapper, insertedEnv, sel);
    }

        if (insertedEnv) insertedEnv.owner.invalidateBehaviorsIfRequired(sSnapperInserted, insertedEnv);
        if (sel) space.select(sel);

MyRowChildWidthChangePropagateFunction changes

Removed field propagateFrames. It was used to block propagating width changes to frame entries. This ability has now been moved out to a new method bool shouldPropagateToEntry(MhEngineEntry entry, MhEngineEntry refEntry) to better control which entry we propagate to.

public class MyRowChildWidthChangePropagateFunction extends MyRowChildChangePropagateFunction {
    Removed: public Bool propagateFrames;

    /**
     * Props.
     */
    public props : cached=true {
        Removed: "propagateFrames";
    }


    Added:
    /**
     * Should propagate to entry?
     */
    extend public bool shouldPropagateToEntry(MhEngineEntry entry, MhEngineEntry refEntry) {
        return refEntry.isBay and entry.isBay;
    }

Propagation changes and alignment of row's children

The propagation mechanism within the racking system has undergone minor revisions. Previously, propagation functions would attempt to reposition the row based on the directional vector of the noticer or refEntry, aiming to maintain alignment across operations. However, this approach proved limiting in multi-bay or multi-frame configurations, where entries within the same row may have differing orientations. Moreover, the previous behavior introduced unnecessary overhead by performing additional row movements solely for alignment purposes.

The rationale behind these changes is to ensure that updates and propagation to affected snappers occur prior to any row adaptation or alignment. This sequencing improves clarity and control over the propagation process, allowing adjustments to be applied more predictably.

MhBayRowEngineBehavior changes

During depth propagation changes, when the system collecting snappers needed to be propagated, the method now will only try to populate affected snappers that will get the changes instead of having additional filter during function execution to filter out non-propagatable snappers.

    /**
     * Append propagation for row children?
     */
    extend public void appendPropagationRowChildren(MhSnapper row, MhSnapperChangedEnv env, str key, MhSnapper{} res) {
	    if (key == "d") {
	        MhSnapper owner = env.owner;
	        if (!owner.isBay and !owner.isFrame) return;

    	    ?MhSnapper parent = owner.parent;
	        Transform t = parent.?isMulti ? parent.?toSpaceTransform() : null;
	    
    	    forChildrenRecursive(MhSnapper c in row) {
		        if (c.isFrame or c.isBay) {
    		        if (!t) {
			            res << c;
		            } else if (c.shapeBound.d == env.oldValue.safeDouble) {
    			        point p = point0.transformed(c.toSpaceTransform() - t);
			            if (p.y == env.lastPos.y) res << c;
		            }
		        }
	        }
	    }
    }

As for the function arguments, the flag moveRow is set to false by default. We're expecting only to update row position and shape during MhRowAlignToChildrenFunction and MhRowUpdateShapeFunction.

	  case "rowChildWidthPropagate", "rowChildDepthPropagate", "rowChildHeightPropagate" : {
	      if (env as MhSnapperChangedEnv) {
		  SnapperSet rows();
		  for (s in propagatingRows(snapper, env)) {
		      // should probably handle aisles in the aisle/flue gap engine behavior.
		      //if (s.rootParent.isAisle) continue;
		      rows << s;
		  }
		  
		  return props { snapper=snapper, noticer=env.owner,
				 rows=rows,
				 moveRow=false };
				 //moveRow=animation.?isStretchAnimation() };
	      }
	  }

MyRowChildWidthChangePropagateFunction changes

Row child propagation will no longer implicitly trigger row movement. Instead, during width propagation, the function now attempts to reposition the propagated entry based on the directional vector of the refEntry. This adjustment clarifies the function’s intent by explicitly aligning movement behavior with reference-based directionality.

public class MyRowChildWidthChangePropagateFunction extends MyRowChildChangePropagateFunction {
    public Object exec() {
        ...

	    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);
	    }   
        ...
    }

MyRowChildDepthChangePropagateFunction changes

Similarly, the depth propagation function will no longer initiate row movement by default during propagation. To streamline its execution and clarify its intended behavior, a new method has been introduced and to opt out from this new approach, please set the return type for this useNewPropagate to false.

public class MyRowChildDepthChangePropagateFunction extends MyRowChildChangePropagateFunction {
    /**
     * Return true if use new depth propagate.
     */
    extend public bool useNewPropagate() {
	    return true;
    }


    /**
     * Propagate depth.
     */
    extend public void propagateDepth2(MhEngine engine, Snapper{} set, MhEngineRowChildEntry refEntry) {
	    MhEngineEntry rootParent = refEntry.rootParent(engine);
	    Transform parentT = refEntry.parent().toSpaceTransform(engine);
	    box refB = refEntry.bound();
	
	    for (MhSnapper s in set) {
	        ?MhEngineBoxEntry entry = engine.entry(s);
	        if (!mhFrameOrBayEntryFilter.accepts(entry)) continue;
	    
	        entry.setD(refEntry.d);

	        if (entry.rootParent(engine) == rootParent) {
		        box b = entry.localBound();
		        b.transform(entry.toSpaceTransform(engine) - parentT);
		        double diff = b.p0.y - refB.p0.y;
		        entry.move((0, -diff, 0));
	        }
	    }
    }

With these changes, the depth propagation function will no longer attempt to rearrange deep racking frame configurations. Its execution is now limited strictly to applying depth changes, minimizing unnecessary operations. Consequently, extensions utilizing deep racking frame setups must explicitly exclude these frames from propagation by applying the following filter when collecting affected objects.

public class DrBayRowEngineBehavior extends MhBayRowEngineBehavior {
    /**
     * Propagating filter.
     */
    public SnapperFilter propagatingFilter(MhSnapper noticer, str key) {
	    SnapperFilter res = super(..);
	    if (key == "d") {
	        if (noticer.isFrame) { // Exclude selection snappers from propagate.
		        return makeCombinedFilter(res, NotSnapperFilter(CurrentSelectionFilter()));
	        } else {
                // Added this filter so depth change will not be applied to the frame.
		        return makeCombinedFilter(res, NotSnapperFilter(frameFilter)); 
	        }
	    }
	    return res;
    }

MhRowChildrenAlignFunction changes

The current alignment implementation is no longer compatible with the newly introduced multi-bay and multi-frame configurations together with non-movable row during propagation. With that, we introduced new concept in the MhRowChildrenAlignFunction class to anchor the affected object and only push next or previous objects while maintaining the position of changed object.

X-axis alignment:

    /**
     * Align X.
     */
    extend public void alignX(sorted double->(MhEngineEntry[]) groupedEntries) {
	    // This is anchor index.
	    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;
	    };

        ...

	    for (x in xs, start=index + 1) {
            // Push objects to the right of index
	    }
	
	    for (x in xs, reverse, start=index - 1) {
            // Push object to the left of index.
	    }
    }

Y-axis alignment:

    /**
     * Align Y.
     */
    extend public void alignY(sorted double->(MhEngineEntry[]) groupedEntries) {
	    // This is anchor index.
	    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.dDiff != 0) result i;
		        }
	        }
	        result 0;
	    };
	
	    double[] xs = groupedEntries.keys;
	    box bound;
	    for (e in groupedEntries.get(xs[index])) if (first) bound = e.bound; else bound += e.bound;

        // Will prioritize to align on the object that changed.
    }

These alignments now can also be performed on different groups (blocks). All the populated entries of the row can be grouped into distinct blocks based on their y-position. This grouping by default will be conditional, only done if a difference in the y-bound of entries at the same x-position is found between entries of the same classification. However the grouping can be always done or always skipped, controlled with the groupByY argument and Bool shouldGroupByY() method. There may be cases where you would want to turn off this grouping, such as deep racking implemented with a non-multiframe structure (one row owns multiple child frames).

    /**
     * Align rows. 
     */
    extend public void alignRows(MhEngineRowEntry row) {
        ...
        if (MhSystemEngineEnvironment env = systemEnvironment()) {
            MhEngineEntryBlock[] blocks = env.getPopulatedBlocks(snapper);
            MhEngineEntryBlock[] blocksToAlign = blocksToAlign(blocks);
            populateDistancesBetweenBlocks(blocksToAlign);
            
            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) {
                    ...
                    groupedEntries.put(x, es);
                    es << e;
                }

                // Align along x-axis.
                alignX(groupedEntries);

                // Align along y-axis.
                alignY(groupedEntries);
            }

            // Align filtered imported block.
            alignBlocks(blocksToAlign);
            
            // Move back to original parent.
            resetToParentTransform(engine, row, blocksToAlign);
        }
    }


    /**
     * Blocks to be align.
     */
    extend public MhEngineEntryBlock[] blocksToAlign(MhEngineEntryBlock[] populated) {
        ...        
        MhEngineEntry[] entries();
        bool grpByY = false;
        Bool forceGroupByY = shouldGroupByY();
        if (forceGroupByY) grpByY = forceGroupByY.v;

        // Collecting all populated entries into a seq.
        for (block in populated) {
            for (e in block.entries) {
                ...
            }
        }

        sorted double->(MhEngineEntry[]) groupedEntries();
        // First pass to create all groups.
        for (MhEngineBoxEntry e in entries) {
            ...
        }

        // Second pass to add other entries that overlap with y to group.
        if (grpByY) {
            ...
        }

        // Create groups sorted by x.
        for (y, es in groupedEntries, index=i) {
            ...
        }
        ...
    }


    /**
     * Should group by y-pos?
     */
    extend public Bool shouldGroupByY() {
        return groupByY;
    }

MhRowAlignToChildrenFunction and MhRowUpdateShapeFunction changes

All logic in this class has been moved up to more generic class to be used by multi-bay and multi-frame as well.

    New: public class MhAlignToChildrenFunction extends MhSystemEngineFunction {
    New: public class MhUpdateShapeFunction extends MhSystemEngineFunction {

MhSystemPopulateFunction changes

Removed argument int adders from method buildRows.

public class MhSystemPopulateFunction extends MhSystemEngineFunction {

    Old: extend public void buildRows(MhSystemEngineEnvironment env, rect r, int adders) {
    New: extend public void buildRows(MhSystemEngineEnvironment env, rect r) {
}

This argument can be replaced with the following code in this method.

    /**
     * BuildOfRows
     */
    extend public void buildRows(MhSystemEngineEnvironment env, rect r) {
        ...
        int adders = info.aisleCount(config);
        if (adders < 0) return;
        ...
    }

cm.abstract.materialHandling.storage.racking.cantilever

Removed class MhCantileverFrameChildAdjustToBehavior

The class MhCantileverFrameChildAdjustToBehavior has been deleted as well as the global singleton mhCantileverFrameChildAdjustToBehavior. The logic within this class has been incorporated into parent class MhChildAdjustToBehavior.

Removed:
public class MhCantileverFrameChildAdjustToBehavior extends MhChildAdjustToBehavior {

    /**
     * Child intersect pos.
     * Position used to find closest snap alternative.
     */
    public point childIntersectPos(MhSnapper owner, MhSnapper child, Object env=null) {
        if (isArm(child)) { // Maintain z-pos of arm regardless of its bound.
            box bound = child.localBound;
            point center = bound.center.transformed(mhTransform(child.pos, child.rot));
            return (center.x, center.y, child.pos.z);
        }

        return super(..);
    }
}

cm.abstract.part

class CustomSpecOption

See cm.abstract.part section in 16.5 New Features migration guide for new CustomSpecOption class documentation .

class AbsPart

Old Pricing API

The following functions are now part of the old pricing system. They are still available for backwards compatibility (except basePrice which has been overloaded in core Part). Migration to the new pricing API should happen as soon as possible. The new pricing API is introduced in v16.5 and documented in the cm.core.part.Part documentation.

public double customListPrice(bool includeChildren=false, str currency=null, Space space=null) {}
extend public double specialListPrice(double listPrice) {}
extend public Double baseOptionPrice() {}
extend public Double specialOptionPrice() {}
extend public double optionSpecialUpcharge() {}
extend public Double upcharge() {}

Old: extend public double basePrice() {}
New: extend public double basePrice(bool includeChildren=false, Space space=null, bool translatePrice=true) {}

class CustomOptionSpecial

See cm.abstract.part section in 16.5 New Features migration guide for new CustomOptionSpecial class documentation .

class SpecOption

Changed: extend public double upcharge(ProdPart owner=null) {}

The upcharge method for SpecOption has been extended to support:

  • A broader Part owner parameter (instead of only ProdPart).
  • Currency translation via a new translatePrice flag and Space context.
Old: extend public double upcharge(ProdPart owner=null) {}
New: extend public double upcharge(Part owner=null, Space space=null, bool translatePrice=false) {}

Old → New behavior

Old:

  • Logic:
    • Started from the option’s price.
    • Applied any applicable OptionSpecial adjustments (replace or add).
    • Returned the resulting value.
  • Only accepted ProdPart as the owner.
  • No currency translation was possible.

New:

  • Logic:
    • Same base calculation (price + special adjustments).
    • If translatePrice=true and the owner uses a non-default currency, the result is converted using owner.translatePrice(...).
  • No longer assumes ProdPart owner.

class OptionSpecial

The following fields and methods have been added and changed in the OptionSpecial class:

// FIELDS
Added: public int level;
Added: public int sequence;

// METHODS
Added: public void copy(PartSpecial special) {}
Added: extend public str optStr=(str value) {}
Added: extend public str optDescStr=(str value) {}
Added: extend public int level=(int value) {}
Added: extend public int sequence=(int value) {}
Added: extend public Part originalPart=(Part value) {}
Added: extend public str originalOptStr=(str value) {}

// UPDATED
Old: public constructor(str partNum, str descr, bool priceReplace, double amount|, str optStr, str optDescStr, Part originalPart, str originalOptStr) {}
New: public constructor(str partNum, str descr, bool priceReplace, double amount|, str optStr, str optDescStr, Part originalPart=null, str originalOptStr=null, int level=0, int sequence=-1) {}

public str flattenableKey() {
	StrBuf res(super());
	res << optStr 
		<< optDescStr 
New:	<< level 
New:	<< sequence
		<< originalPart 
		<< originalPart.specialsKey();
	return res.toS();
}

class ProdPart

Changed: final public SpecOption[] specOptions=(SpecOption[] newOptions) {} and extend public void appendSpecOptions(SpecOption[] newOptions) {}

Both specOptions and appendSpecOptions now accept a new boolean parameter invalidateOptionPrice (default: true). This works with the new option price caching mechanism by allowing the system to selectively invalidate the cached option price sum when spec options are modified.

Old: final public SpecOption[] specOptions=(SpecOption[] newOptions) {}
New: final public SpecOption[] specOptions=(SpecOption[] newOptions, bool invalidateOptionPrice=true) {}

Old: extend public void appendSpecOptions(SpecOption[] newOptions) {}
New: extend public void appendSpecOptions(SpecOption[] newOptions, bool invalidateOptionPrice=true) {}
Old → New behavior

Old:

  • Option prices were not cached.
  • Every time option prices were accessed, they were recalculated on the fly based on current spec options.
  • No invalidation step was needed.

New:

  • Option price totals are now cached
  • When spec options are modified, invalidateOptionPriceSum() is triggered to ensure the cache stays correct
  • Developers can pass invalidateOptionPrice=false to skip cache invalidation if they plan to manage it manually or delay recalculation.
Impact
  • Performance improvement: frequent option price sum lookups no longer require full recalculation
  • Cache management: introduces the concept of invalidating/regenerating option price sums as options change.
  • Default safety: with the default true, cache stays consistent automatically — existing usage behaves as expected
  • Advanced optimization: developers can set false during bulk option appends/updates, then explicitly trigger invalidation once, avoiding multiple regenerations.
  • Migration note: developers should be aware that option pricing is no longer computed “live” — results are now cache-backed.

Other ProdPart Changes

  • Added optional bool invalidateWorldPrice parameter to special functions
    • Allows control over invalidation of world pricing when making a special change
    • Originally invalidated world price on every special change
  • Expanded special interfaces to accept PartOptionItems
    • Previously was exclusively accepted SpecOptions
Interfaces
Added: extend public void putOptSpecial(PartOptionItem opt, OptionSpecial special, PropObj s=null, bool invalidateWorldPrice=true) {}

Old: extend public void generateOptAdjustmentSifRows(SpecOption opt, str[] lines) {}
New: extend public void generateOptAdjustmentSifRows(PartOptionItem opt, str[] lines) {}

Old: extend public str optSpecialKey(SpecOption opt) {}
New: extend public str optSpecialKey(PartOptionItem opt) {}

Old: extend public OptionSpecial getOptSpecial(SpecOption opt, PropObj s=null) {}
New: extend public OptionSpecial getOptSpecial(PartOptionItem opt, PropObj s=null) {}

Old: extend public void removeOptSpecial(SpecOption opt, PropObj s=null) {}
New: extend public void removeOptSpecial(PartOptionItem opt, PropObj s=null, bool invalidateWorldPrice=true) {}

Old: extend public void removeOptSpecials(PropObj s=null) {}
New: extend public void removeOptSpecials(PropObj s=null, bool invalidateWorldPrice=true) {}

Old: extend public void removeAllSpecials(PropObj s=null) {}
New: extend public void removeAllSpecials(PropObj s=null, bool invalidateWorldPrice=true) {}

class ProdPartGridBuilderEnv

Added: extend public GridCell getOptionRowCell(PartOptionItem opt, int columnIdx, Part part=null) {}

  • Gets option row cell for parameter values
  • Parameters:
    • PartOptionItem opt: Option for row to get cell for (can be null)
    • int columnIdx: Column index to get cell for
    • Part part: Part owning option row to get cell for (can be null)
      • mainly utilized for acquiring option specials data
Behavior
  • If opt is null or column index is out of bounds, returns getDefaultCell
  • Otherwise, returns a cell for the option data associated with the column index
    • Assigns a background color to the cell by calling getCellBrush

Changed: extend public GridCell[] getOptionRowCells(SpecOption opt, ProdPart part=null) {}

Old → New behavior

Old:

  • Directly returned a sequence of GridCells

New:

  • Loops through columns, calling getOptionRowCell for each column and appending it to the returned GridCell sequence
  • Allows for more flexibility when building option row cells

cm.abstract.part.query

class OptionMakeSpecialDialog

Removed: public void onOKButtonClick(Object sender, Object args) {}
  • This method was originally overridden to allow for returning an OptionSpecial type on OK button click
  • The super class now calls generateSpecial to create the new special on OK button click

class QueryProdPartGridBuilderEnv

In 16.5, the ProdPartQueryDialogDataEnv has changed it's backing data structures, replacing the str->Part parts, int->str rowIDs, and str->str{} options maps with a single QueryRowData[] queryRows data sequence. Cell highlighting has also been introduced in the QueryDialog.

As a result, the following functions have been removed and added to QueryProdPartGridBuilderEnv.

Removed: public str rowIdentifier(Object data) {}
Added: public GridCell[] getRowCells(Object data) {}
Added: public Brush getCellBrush(Object data, int columnIdx) {}

class ProdPartQueryDialogBehavior

New Add Option Feature

With the addition of the new "Add Option" feature in the query dialog and the new data structure of QueryDialogDataEnv, changes in ProdPartQueryDialogBehavior have been made.

  • Overrides of top window related functions:
    • initTopWindow to utilize new ProdPartQueryControlWindow for ProdParts
    • initTopWindowEvents to handle events from ProdPartQueryControlWindow
    • onAddOptionBtnClicked to handle add option button click from ProdPartQueryControlWindow
  • Overrides of new getCustomSpecialDialog function:
    • Overidden to return custom special dialog for a ProdPart option
  • Modified interfaces to support new index-driven data model (vs. previous str-based data model)
    • Shown below

The following interface additions, removals, and changes have been made in ProdPartQueryDialogBehavior:

// ADDED/REMOVED INTERFACES
Added: public SubWindow initTopWindow(QueryDialog parent) {}
Added: public void initTopWindowEvents(SubWindow topWindow) {}
Added: public DialogWindow getCustomSpecialDialog(QueryDialog dialog, int row) {}
Added: extend public void onAddOptionBtnClicked(Object sender, Object args) {}
Removed: extend public str getPartIDFromOptionID(QueryDialog dialog, str id) {}
Removed: extend public Part getPartFromOptionID(QueryDialog dialog, str id) {}

// MODIFIED INTERFACES
Old: extend public str getOptCodeFromGrid(QueryDialog dialog, str id) {}
New: extend public str getOptCodeFromGrid(QueryDialog dialog, int row) {}

Old: extend public str getOptDescriptionFromGrid(QueryDialog dialog, str id) {}
New: extend public str getOptDescriptionFromGrid(QueryDialog dialog, int row) {}

Old: public DialogWindow getSpecialDialog(QueryDialog dialog, str id) {}
New: public DialogWindow getSpecialDialog(QueryDialog dialog, int row) {}

Old: extend public DialogWindow getOptionSpecialDialog(QueryDialog dialog, str id |, Part part) {}
New: extend public DialogWindow getOptionSpecialDialog(QueryDialog dialog, int row |, Part part) {}

class ProdPartQueryDialogDataEnv

With the new data structure of QueryDialogDataEnv, changes in ProdPartQueryDialogDataEnv have been made.

  • New data building functions:
    • buildQueryRowData: Overridden to support ProdPart option rows
    • buildPartOptions: Created to build part option rows for ProdParts
    • createPartRowData: Overridden to create QueryProdPartRowDatas for ProdPart rows
    • createOptionRowData: Created to build QueryOptionRowDatas for ProdPart option rows
    • refreshQueryRowData: Overridden to optimize rebuild process
  • Removed public str->str{} options field:
    • No longer needed with new data structure
    • Option rows are stored in new QueryRowData[] queryRows as QueryOptionRowDatas
  • Removed old data building/manipulation functions:
    • setParts: No longer needed with new data structure
    • buildParts: ''
    • buildPartOptions: ''
    • getPartIDFromOptionID: ''
    • getPartFromOptionID: ''
    • getSpecial: Logic has been moved to new QueryRowData type
    • putSpecial: ''
    • removeSpecial: ''

The following interface additions and removals have been made in ProdPartQueryDialogDataEnv:

// ADDED INTERFACES
Added: public void buildQueryRowData(Part[] parts=null) {}
Added: extend public void buildPartOptions(QueryRowData partRow) {}
Added: public QueryRowData createPartRowData(Part part) {}
Added: extend public QueryRowData createOptionRowData(SpecOption option, QueryRowData partRow) {}
Added: public void refreshQueryRowData() {}

// REMOVED INTERFACES
Removed: public str->str{} options;
Removed: public Part[] setParts(Part[] value) {}
Removed: public void buildParts() {}
Removed: extend public void buildPartOptions(str partRowID, ProdPart part) {}
Removed: extend public void buildPartOptions(ProdPart part) {}
Removed: public PartSpecial getSpecial(str id) {}
Removed: public void putSpecial(str id, PartSpecial special) {}
Removed: public void removeSpecial(str id) {}
Removed: extend public str getPartIDFromOptionID(str id) {}
Removed: extend public Part getPartFromOptionID(str id) {}

cm.abstract.tools.elevationG2

ElevArrowG2

The field elevSpace has changed its type from Space to a more specific ElevSpace. This reduces the need for casting.

Old: public Space elevSpace : stream=null, copy=null;
New: public ElevSpaceG2 elevSpace : stream=null, copy=null;

# Example
Old: if (elevSpace as ElevSpaceG2) {
New: if (elevSpace) {

ElevAutoSnapperEnvG2 changes

A new field bool locationLocked has been added. As such, the constructor has been updated to set this field.

public class ElevAutoSnapperEnvG2 extends PropObj {
    Old: public constructor(str key, point pos, orientation rot) {
    New: public constructor(str key, point pos, orientation rot, bool locationLocked=false) {
}

The class ElevAutoDimEnvG2 has also had its constructor updated to set this field.

public class ElevAutoDimEnvG2 extends ElevAutoSnapperEnvG2 {
    Old: public constructor(str key, point p0, point p1, double d, UserTextStyle ts, UserDimensionStyle ds, str prefix="", bool allowDimSelect=true) {
    New: public constructor(str key, point p0, point p1, double d, UserTextStyle ts, UserDimensionStyle ds, str prefix="", bool allowDimSelect=true, bool locationLocked=false) {
}

cm.abstract.unitLoad

UnitLoad changes

Moved a method Transform loadDirectionXForm() down from UnitLoadPallet to UnitLoad.

public class UnitLoad extends CoreObject {

    /**
     * Transform for load direction.
     */
    extend public Transform loadDirectionXForm() {
        return null;
    }
}

Updated interface of method bool eq. Added a new optional argument str->Object args=null.

public class UnitLoad extends CoreObject {
    Old: extend public bool eq(UnitLoad m) {
    New: extend public bool eq(UnitLoad m, str->Object args=null) {
}

At the moment, only one value in args is used in this method, "propsToIgnore"->str[] which can be used to ignore specific props in specific equality checks. It serves as an alternative to overriding str[] propsIgnoreEqCheck() which will ignore the props for all equality checks of that UnitLoad.

public class UnitLoad extends CoreObject {

    /**
     * Equal?
     */
    extend public bool eq(UnitLoad m, str->Object args=null) {
        ...
        ?str[] propsToIgnore = args.?get("propsToIgnore");
        if (propsToIgnore) propsToIgnore += propsIgnoreEqCheck();
        else propsToIgnore = propsIgnoreEqCheck();
        for (k in propDefs.order) {
            if (k in propsToIgnore) continue;
            ...
        }
    }
}

As a result of this change, we have also removed the function bool unitLoadEq(UnitLoad unitLoad, UnitLoad l). Previous calls to this function have been replaced by directly calling the eq(UnitLoad m, str->Object args=null) method.

Removed:
public bool unitLoadEq(UnitLoad unitLoad, UnitLoad l) {
    return unitLoad.eq(l);
}

Example usage:
unitLoad.eq(l);

Added a new method MhPalletLoadSide getLoadDirection() so that UnitLoad can return a direction value. This value was previously only used in UnitLoadPallet but now there is an interface so that other unit load classes can return a direction as well.

public class UnitLoad extends CoreObject {

    /**
     * Get load direction.
     */
    extend public MhPalletLoadSide getLoadDirection() {
        return null;
    }
}

UnitLoadPreviewSnapper changes

Updated the interface of method Primitive3D loadDir3D. The argument UnitLoadPallet pallet has been widened to UnitLoad ul.

public class UnitLoadPreviewSnapper extends UnitLoadSnapper {
    Old: extend public Primitive3D loadDir3D(UnitLoadPallet pallet) {
    New: extend public Primitive3D loadDir3D(UnitLoad ul) {
}

Previous implementations of loadDir3D(UnitLoadPallet ul) would often access a direction value mhPalletLoadSide that was previously only available to UnitLoadPallet. You can switch those calls over to UnitLoad.getLoadDirection() instead without needing to cast to UnitLoadPallet.

Example:
public class UnitLoadPreviewSnapper extends UnitLoadSnapper {

    /**
     * Load Direction Arrow 3D.
     */
    extend public Primitive3D loadDir3D(UnitLoad ul) {
        MhPalletLoadSide dir = ul.getLoadDirection();
        if (!dir) return null;
        ...
    }
}

UnitLoadDialog changes

Updated the interface of method void zoomToPreview. The argument Snapper s has been removed. The reason for this is we have switched the previous implementation of zooming in to a specific snapper's bound to a zoomExtents of the view.

public class UnitLoadDialog extends DialogWindow {

    Old:
    extend public void zoomToPreview(Snapper s) {
        if (!preview3D or !preview3D.space) return;
        if (!s) {
            for (ns in preview3D.space.snappers) {
                s = ns;
            }
        }

        if (!s) return;

        disableUndo {
            preview3D.zoomBound(s.bound);
        }
    }


    New:
    extend public void zoomToPreview() {
        if (!preview3D or !preview3D.space) return;
        if (preview3D.space.snappers.empty) return;
        preview3D.zoomExtents(undoable=false);
    }
}

UnitLoadTemplateSelectorDialog changes

Updated the interface of method void zoomToPreview. The argument Snapper s has been removed. The reason for this is we have switched the previous implementation of zooming in to a specific snapper's bound to a zoomExtents of the view.

public class UnitLoadTemplateSelectorDialog extends DialogWindow {

    Old:
    extend public void zoomToPreview(Snapper s) {
        if (!preview3D or !preview3D.space) return;
        if (!s) {
            for (ns in preview3D.space.snappers) {
                s = ns;
            }
        }

        if (!s) return;

        disableUndo {
            preview3D.zoomBound(s.bound);
        }
    }


    New:
    extend public void zoomToPreview() {
        if (!preview3D or !preview3D.space) return;
        if (preview3D.space.snappers.empty) return;
        preview3D.zoomExtents(undoable=false);
    }
}

Renamed class MhUserSettings to UnitLoadUserSettings

The class MhUserSettings has been renamed to UnitLoadUserSettings.

The global function MhUserSettings mhUserSettings() has been replaced by UnitLoadUserSettings ulUserSettings().

Old: public class MhUserSettings extends UserSettings {
New: public class UnitLoadUserSettings extends UserSettings {

Old: public MhUserSettings mhUserSettings() {
New: public UnitLoadUserSettings ulUserSettings() {

cm.application

Removal of StdApplicationManager

The class StdApplicationManager and ApplicationManager has been combined into a single class now. This should simplify existing code by eliminating the need to cast (a sample example is listed).

Old: public class StdApplicationManager {
New: public class ApplicationManager {

# Example
Old: app = appManager.StdApplicationManager.?getApplication(id) ?? application;
New: app = appManager.?getApplication(id) ?? application;

notifyStrip.cm

Added new arguments to addButton method to control the standard brush and the hover brush of buttons in the notify strip.

Old: extend public NotifyPartButton addButton(str text, str tooltip=null,
					      function(Control, NotifyButtonArgs) callback=null,
					      Object callbackArg=null,
					      bool closeAfterClick=false,
					      Timespan snoozeAfterClick=null,
					      function():Timespan snoozeAfterClickCallback=null,
					      bool highlight=false,
					      bool triggerCallbackOnShow=false,
					      Image image=null)
New: extend public NotifyPartButton addButton(str text, str tooltip=null,
					      function(Control, NotifyButtonArgs) callback=null,
					      Object callbackArg=null,
					      bool closeAfterClick=false,
					      Timespan snoozeAfterClick=null,
					      function():Timespan snoozeAfterClickCallback=null,
					      bool highlight=false,
					      bool triggerCallbackOnShow=false,
					      Brush customStdBrush=null,
					      Brush customHoverBrush=null,
					      Color customLabelColor=null,
					      Image image=null)

notifyMessage.cm

Added new arguments to the constructor of NotifyPartButton to store the standard brush and the hover brush when creating buttons for the notify strip.

Old: public constructor(NotifyMessage message, str text,
		       str tooltip=null,
		       function(Control button, NotifyButtonArgs args) callback=null,
		       Object callbackArg=null,
		       bool closeAfterClick=false,
		       // MergeTODO : add a close callback here in 8.0
		       Timespan snoozeAfterClick=null,
		       function():Timespan snoozeAfterClickCallback=null,
		       bool highlight=false,
		       bool triggerCallbackOnShow=false,
		       Image image=null)
New: public constructor(NotifyMessage message, str text,
		       str tooltip=null,
		       function(Control button, NotifyButtonArgs args) callback=null,
		       Object callbackArg=null,
		       bool closeAfterClick=false,
		       // MergeTODO : add a close callback here in 8.0
		       Timespan snoozeAfterClick=null,
		       function():Timespan snoozeAfterClickCallback=null,
		       bool highlight=false,
		       bool triggerCallbackOnShow=false,
		       Brush customStdBrush=null,
		       Brush customHoverBrush=null,
		       Color customLabelColor=null,
		       Image image=null)

cm.core

class Entity

Force initAfterInstantiate() to call ancestor super class in order to ensure gid protocol is always respected.

Old: extend public void initAfterInstantiate() {
New: extend public void initAfterInstantiate() : require super {

copySelection.cm

The restoreCleanup() function has been removed. Its primary functionality made unexpected modifications to the snapper being copied. As a replacement to calling restoreCleanup(), use if (Space space = getSpace()) space.select(SpaceSelection(this)); to restore the original selection.

If modifications to the original snapper is required for some reason (which is highly discouraged), consider making use of the snapper functions beforeUserCopied(..) or userCopied(..) instead.

Removed: final public void restoreCleanup() {

CET QL

In the class CetQLWorldManager, the method destroySpace has been renamed to removeSpace.

Old: extend public void destroySpace(Space s) {
New: extend public void removeSpace(Space s) {

In the class CetQLSpaceManager, the field removeSnapperSet has been removed.

Old: public SnapperRef{} removeSnapperSet : deprecated;

SnapperRef

As SnapperRef has been reworked to also keeps track of the space guid to enforce greater consistency, we have a new and recommended way to construct a SnapperRef.

Old: public constructor(guid gid) {
Old: public constructor(Snapper z) {
New: public constructor(Snapper z, Space space) {

ToolbarModelItem

The access modifiers for field image has been changed, a new setter method is introduced.

Old: public Image image : copy=reference;
New: private Image image : copy=reference, public readable;
New: final public void setImage(Image i) {

class Space

A new function for invalidating ItemTags in a space has been added.

  • Iterates ItemTags in bsp.singularities, calling invalidate(..) on each
  • Iterates BlockSpaces, calling invalidateItemTags(..) on each
/**
 * Invalidate all item tags in BSP.
 */
extend public void invalidateItemTags(dirty2D flag2D=dirty2D.all) {
	for (ItemTag s in bsp.?singularities) s.invalidate(flag2D);
	
	for (BlockSnapper blockSnapper in snappers) {
		BlockSpace blockSpace = blockSnapper.block.?blockSpace;
		blockSpace.?invalidateItemTags(..);
	}
}

cm.core.block

class BlockSpace

A new function for invalidating BlockSpace ItemTags has been added.

  • Overidden from the super classes implementation
  • Calls block.?updateOwners(..) after the invalidation has been done
/**
 * Invalidate all item tags in BSP.
 */
public void invalidateItemTags(dirty2D flag2D=dirty2D.all) {
	super(..);
	block.?updateOwners(recursive=false, excludedSpace=this, flag2D=flag2D, flag3D=dirty3D.none);
}

cm.core.collabG3

# CollFilesDialogResizer
Removed: resize
Removed: needUpdateOnResize

Added: resize0
Added: resize1

cm.core.collabG3.ui

CollProjectTvItem

The constructor for CollProjectTvItem has been updated to always show customer information by default. Additionally, the constructor now allows optional configurations like bolding the labels for active projects and whether the tree view item itself should be clickable or not.

Old: public constructor(CollProjectInfo project,
                        bool showCustomer=false, 
                        bool showLastModified=true) {

New: public constructor(CollProjectInfo project,
                        bool showCustomer=true,
                        bool showLastModified=true,
                        bool boldActive=true,
                        bool allowClick=true) {

The following mouse operation handling methods have been removed as the tree view item no longer displays a CollProjectTVITag beside local CollabPro projects.

Removed: public bool enter(TreeViewMouseInfo mi) {}
Removed: public bool exit(TreeViewMouseInfo mi) {}
Removed: public bool move(TreeViewMouseInfo mi) {}

FilePageCollaboration

The following class field has been removed and dereferenced from the FilePageCollaboration dialog.

Removed: public Button refreshProjBtn;

As part of our efforts to standardize the layout and interface of CollabPro's file menu page, the following build and relayout methods have been removed. To further extend from the FilePageCollaboration dialog, additional customized build methods should be supplied by overriding the main build() method instead. For customized relayouts, they should be included by overriding the main relayout() method as well.

Removed: extend public void buildInfoSW() {}
Removed: extend public void buildContentSW() {}
Removed: extend public void buildButtons(Brush brush) {}
Removed: extend public void buildSearch(Brush brush) {}
Removed: extend public void relayoutInfoSW() {}
Removed: extend public void relayoutContentSW() {}

cm.core.dwg

class DwgBsp

Removed an argument that was not used

Old: final public void endModify(ProgressDialog pd) {
New: final public void endModify() {

dwgDialog.cm

Old: public Snapper loadDwgFiles(Url[] files, DropFiles drop=null, Point dropSpacePos=null, Point pos=null, bool exception=false) {
New: public Snapper{} loadDwgFiles(Url[] files, DropFiles drop=null, Point dropSpacePos=null, Point pos=null, bool exception=false) {

class DwgDialog

Many interfaces have been changed to accommodate the new multi-select functionality. Most notably, a lot of functions now take a Snapper argument that they operate on instead of always operating on the selected DWG. This makes it possible to operate on individual DWGs in the selection when necessary.

The following interfaces that were used to get the currently selected DWG have been updated to return the set of all selected DWGs.

Old: package Snapper activeDwgSnapper : public readable;
Old: final public Snapper snapper() {
New: final public Snapper{} selection() {
Old: extend public DwgTvi selectedTvi() {
New: final public DwgTvi{} selectedTVItems() {

The following interface that was used to select a DWG has been updated to also handle appending to selection. The reason for it being 'smart' is that it is not a simple select. It can pop up a modal dialog and keep modified DWGs in selection even if removePrevious=true.

Old: final public void changeActiveDwg(Snapper s, bool rebuild=true) {
New: final public bool smartSelect(Snapper dwgSnapper, bool removePrevious=true, bool invokeCallback=true) {

To be able to select many DWGs at once the following interfaces have been added.

Added: final public bool smartSelect(Snapper{} dwgSnappers, bool removePrevious=true, bool invokeCallback=true) {
Added: final public bool smartSelect(str{} keys, bool removePrevious=true, bool invokeCallback=true) {

The dwgSnapperIsParent-field has been removed as it is no longer needed. To figure out if a tree view item is a parent it is possible check item in DwgParentTvi instead. Selected parent tree view items will not contribute to final public Snapper{} selection().

Removed: private bool dwgSnapperIsParent : public readable;

The following methods are deemed no longer needed and have been removed. There is no direct replacement for them.

Removed: final public void setScaleFromInsUnits(bool update=true) {
Removed: final public Snapper snapperFromItem(TreeViewItem item) {
Removed: final public str getUnitKey() {

The fileTreeViewChanged()-function has been removed. To update the file tree view, call updateFileTreeView or updateContent instead.

Removed: final public void fileTreeViewChanged() {

These methods can be replaced with methods with the same name on DwgCaptureSnapper.

Removed: final public bool sourceFileMissing() {
Removed: final public bool sourceFileOutOfDate() {

The isCaptureSnapper()-function has been removed. As a replacement, check snapper in DwgCaptureSnapper instead.

Removed: final public bool isCaptureSnapper() : inline {

The getDwgSource()-function has been removed. Use the getSource()-function on DwgSnapper or DwgCaptureSnapper instead.

Removed: final public Url getDwgSource() {

The getDwgRoot()-function has been removed. Use the getDwgRoot()-function in cm/core/dwg/dwg.interface.cm instead.

Removed: final public DwgRoot getDwgRoot() {
Old: private bool dwgSnapperHasMultipleCopies : public readable;
New: final public bool hasMultipleCopies(Snapper snapper) {
Old: extend public bool apply(Point pos=null, bool exception=false) {
New: extend public bool apply(Snapper snapper, Point pos=null, bool exception=false) {
Added: extend public bool apply(Snapper{} snappers, Point pos=null, bool exception=false) {

The functions below have been updated to return the loaded DWG snappers, in order to improve code readability. For the method that returned a bool, the new will return null if and only if it previously returned false.

Old: extend public bool loadDwgFile(Url file, bool xref=false, Window parent=null,
New: extend public Snapper loadDwgFile(Url file, bool xref=false, Window parent=null,

Old: final public void loadDwgFile(Url file, DwgRoot root, bool xref=false, bool exception=false, bool dragDrop=false) {
New: final public Snapper loadDwgFile(Url file, DwgRoot root, bool xref=false, bool exception=false, bool dragDrop=false) {

Old: final public void animate(Url file, bool repeat=true, Window parent=null, DropFiles drop=null, Point dropSpacePos=null) {
New: final public Snapper animate(Url file, bool repeat=true, Window parent=null, DropFiles drop=null, Point dropSpacePos=null) {

A Snapper argument has been added to replace uses of the activeDwgSnapper-field in the following methods. The argument expects either a DwgSnapper or a DwgCaptureSnapper.

Old: extend public bool importSettingsChanged() {
New: extend public bool importSettingsChanged(Snapper snapper) {

Old: final public DwgSnapper{} getXRefsNoInsert() {
New: final public DwgSnapper{} getXRefsNoInsert(Snapper snapper) {

Old: final public void groupActiveDwgSnapperWith(Snapper{} snappers) {
New: final public void groupDwgSnappers(Snapper main, Snapper{} snappers) {

Old: extend public int checkForChanges() {
New: extend public int checkForChanges(Snapper{} snappers=dwgDialog.?selection()) {

Old: final public void dropOnCursor(DropFiles drop, Point spacePos) {
New: final public void dropOnCursor(Snapper snapper, DropFiles drop, Point spacePos) {

Old: extend public void locateSnapper() {
New: extend public void locateSnapper(Snapper snapper) {

Old: extend public void reloadSnapper(Url u=null) {
New: extend public void reloadSnapper(Snapper snapper, Url u=null) {

Old: extend public void reloadDwgSnapper(Url u=null) {
New: extend public void reloadDwgSnapper(Snapper snapper, Url u=null) {

Old: extend public void reloadCaptureSnapper(Url u=null) {
New: extend public void reloadCaptureSnapper(Snapper snapper, Url u=null) {

Old: extend public void renameSnapper(str newName=null) {
New: extend public void renameSnapper(Snapper snapper, str newName=null) {

Remove methods

Remove an item from the dwg dialog:

Old: extend public void removeSnapper(Snapper candidate, bool update=true, bool anySnapper=true) {
New: extend public void removeSnapperFromDialog(Snapper candidate, bool update=true) {

Remove item from space and dwg dialog:

Remove: extend public void removeSnapper() {

To remove from space use removeDwgSnapper(Snapper snapper) instead.

class DwgHideLayerOp

Renamed variable for clarity

Old: public int64{} l;
New: public int64{} layers;
Old: public constructor(DwgSnapper dwg, int64 l, bool undo=true) {
New: public constructor(DwgSnapper dwg, int64{} layers, bool undo=true) {

class DwgLayersCard

Many methods now take an argument to specify the DwgSnapper being edited.

Old: final public void setSnappableLayer(DwgLayer z, bool v) {
New: final public void setSnappableLayer(DwgLayer z, bool v, Snapper snapper) {
New: final public void setSnappableLayer(DwgLayer z, bool v, Snapper{} snapper) {

Old: final public void setForceLayer(DwgLayer z, bool v) {
New: final public void setForceLayer(DwgLayer z, bool v, Snapper snapper) {
New: final public void setForceLayer(DwgLayer z, bool v, Snapper{} snapper) {

Old: final public void setLayerColor(DwgLayer{} layers, color c, bool ignoreUserMaterial) {
New: final public void setLayerColor(DwgLayer{} layers, color c, bool ignoreUserMaterial, Snapper snapper) {
New: final public void setLayerColor(DwgLayer{} layers, color c, bool ignoreUserMaterial, Snapper{} snapper) {

Old: final public void setLayerColor(DwgLayer layer, color c, bool ignoreUserMaterial) {
New: final public void setLayerColor(DwgLayer layer, color c, bool ignoreUserMaterial, Snapper snapper) {

One method was given the responsibility of finding all snappers being updated itself, since it needed this to create the proper undo steps:

Old: final public void setVisibleLayer(DwgLayer z, bool v, bool updateVisibility=true) {
New: final public void setVisibleLayer(bool v, DwgLayerTreeViewItem clickedItem, bool updateVisibility=true) {

class DwgSettingsEnv

Removed: extend public bool equal(DwgSettingsEnv target, bool transforms=true, bool units=true, bool scale=true, bool cap=true, bool import3D=true, bool layers=false, bool captureGraphics=true) {
Old: extend public bool equal(DwgSettingsEnv target, bool transforms=true, bool units=true, bool scale=true, bool cap=true, bool import3D=true, bool layers=false, bool captureGraphics=true, bool explode=true) {
New: extend public bool equal(DwgSettingsEnv target, bool transforms=true, bool units=true, bool scale=true, bool cap=true, bool import3D=true, bool layers=false, bool captureGraphics=true, bool explode=true, bool xrefs=true) {

class DwgTreeView

Old: extend public void update(bool recursive=true) {
New: extend package void update(bool recursive=true) {

class DwgSnapperCreator

Removed: final public DwgSnapper{} insertAllXRefs(Window parent=null, Space space=null, bool relocate=true) {

Instead use

final public DwgSnapper{} insertAllXRefs(Window parent=null, Space space=null, bool relocate=true, bool insert=true, Bool xRefImportCancelled=null) {

class DwgPropertiesCard

Some interfaces have been changed from public to private as they are not intended to be used outside of DwgPropertiesCard.

Old: final public SubWindow buildTopWin(Window parent) {
New: final private SubWindow buildTopWin(Window parent) {

Old: final public void updateSettingsDrop() {
New: final private void updateSettingsDrop(DwgPropertiesCardEnv env=null) {

Old: final public void updateContent(bool scrollToLatestPos=false) {
New: final public void updateContent(Snapper{} snappers, bool scrollToLatestPos=true) {

Old: final public void setPresetScale(double scale) {
New: final public void setPresetScale(double scale, bool invokeCallback=true) {

Old: final public str getUnitKey() {
New: final private str getUnitKey() {

Removed: final public void updateDisplays() {
Added: final private void updateActualScaleDisplay() {
Added: final private void updateSizePreviewDisplay(size dims) {
Added: final private void updateActualDimensionDisplay(size dims) {

Old: final public double getActualScale() {
New: final private double actualScale() {

Removed: final public void setMessage(str msg) {
Removed: final public void updateMessage(bool update=true) {
Removed: final public str updateMessage2() {
Added: final private void updateInfoDisplay(DwgPropertiesCardEnv env) {

The following methods are deemed no longer needed and have been removed. There is no direct replacement for them.

Removed: final public bool enablePresetsDD(Snapper selection) {
Removed: extend public double getUnitValue () {
Removed: final public void setRawScale() {

The reBuild()-function is no longer needed, since updateContent() now updates all UI elements.

Removed: final public void reBuild() {

The setScaleFromInsUnits()-function is no longer needed and has been removed.

Removed: final public void setScaleFromInsUnits(bool update=false) {
Old: extend public void fitToPaper() {

Use these instead (in cm/core/dwg/dwgPropertiesCard.cm)

New: public void fitToPaper(Snapper snapper, Space insertSpace=null) {
New: public void fitToPaper(Snapper{} snappers=null, Space insertSpace=null) {

class DwgHideLayerOp

The undo operation can now undo multiple layers at once.

Old: public int64 l;
New: public int64{} layers;

Old: public constructor(DwgSnapper dwg, int64 l, bool undo=true) {
New: public constructor(DwgSnapper dwg, int64{} layers, bool undo=true) {

class DwgExporter

renamed styles map and made it package

Old: public str->DwgStyle styles();
New: package str->DwgStyle nameStyleMap();

cm.core.favorite

class FavoriteCache

Return DibImage for the use of SaveFavoriteDialog and BrowseFavoriteDialog

Old: final public MemoryImage thumbnail(Url location) {
New: final public DibImage thumbnail(Url location) {

class FavoriteInfo

Change to store DibImage for the use of FavoriteCache, reducing GDI Object usage.

Old: public MemoryImage image;
New: public DibImage image;

favorites.cm

MemoryImage to be casted to DibImage and destroyed explicitly to reduce GDI Object usage.

Old: public MemoryImage favoriteThumbnail(Url location, bool progress=false) {
New: public DibImage favoriteThumbnail(Url location, bool progress=false) {

cm.core.geometry2D.advanced

class APath2D

The area method in APath2D has now been implemented.

public double area() {

Therefore, this approximate area method has been removed.

Removed: extend public double area(angle arcResolution=15 deg, double deviationTol=0.1) {

cm.core.init

class LeadTimeColumn

As of 16.5, a new interface for lead time has been added to core Part. As a result, a new lead time column has been created in cm/core/init/leadTimeColumn.cm. Extensions who have their own Lead Time column should migrate to the core version to avoid duplication.

Developers can migrate by:

  • Preferred option:
    • If applicable, remove registry of a custom Lead Time Column
    • Implement leadTime() in Part to get accurate value for core Lead Time column
  • Secondary option:
    • If custom lead time column exists...
      • Remove registry of a custom Lead Time Column
      • Utilize overridePartColumns() on Part to return custom lead time column in place of core column
/**
 * Lead Time Column for Parts
 */
public class LeadTimeColumn extends BasicPartColumn {

    /**
     * Constructor.
     */
    public constructor() {
		super("coreLeadTime");
    }
    

    /**
     * Initial Visibility.
     */
    public symbol[] initialVisibility() {
		return [#none];
    }


    /**
     * Return property value for part.
     */
    public Object value(Part part, Space space) {
        return part.leadTime();
    }
}

cm.core.itemTag

hooks.cm

A new hooks.cm file has been added to the cm/core/itemTag package. The public functions below have been moved to it from the itemTag.cm file.

public bool updateWorldItemTags(World world, bool validate, PriceChangeEnv env) {}
public bool updateSpaceItemTags(Space space, bool validate, function():bool interrupt, bool force) {}
public void updateSpaceItemTags(Space space) {}

class ItemTagInfo

A new field str articleCodeText has been added in ItemTagInfo.

  • Holds the article code value of the Part associated with the ItemTag
  • Allows for the new control panel setting toggling article codes as Ind tags (only visually, not on export)
  • New getTagText() function returns ItemTagInfo text based on setting
    • See runtime behavior documentation for ItemTagInfo

The fields value is set in core Part during updateItemTags() or it can be set on construction with this new constructor:

/**
 * Creates a new ItemTagInfo.
 */
public constructor(str text, str k, str textType, str articleCode) {
	articleCodeText = articleCode;
	this(text, k, textType);
}

The new getTagText() function checks the setting value to return either the articleCodeText or the tagText:

/**
 * Get tag text.
 */
extend public str getTagText() {
	if (displayArticleCodeForItemTag) return articleCodeText;
	return tagText;
}

functions.cm

A new functions.cm file has been added to the cm/core/itemTag package. The public functions below have been moved to it from the itemTag.cm file.

public void enableItemTagsAlwaysUpdate(bool on) {}
public bool itemTagsAlwaysUpdateEnabled() {}
public void removeItemTags(Snapper this) {}

cm.core.part

class PartProxy

The following functions were added to PartProxy in v16.5. These functions were added in support of the new pricing model which is documented in the compile-time section for cm.core.part.Part.

New: extend public double basePrice(Part part, bool includeChildren=false, Space space=null, bool translatePrice=true) {}
New: extend public double optionPriceSum(Part part, bool includeChildren=false, Space space=null, bool translatePrice=true, bool invalidate=false) {}

class Part

Constructor Migration: List Price → Base Price + Option Price Sum

In v16.5, a new constructor has been introduced for Part to support the new pricing model. Previously, Part accepted a cached list price (listPrice) directly as a parameter. This behavior is now being phased out.

Old Constructor
  • constructor(Snapper snapper, str articleCode, str description, double listPrice, ...)
  • Relies on cached listPrice.
  • Marks Part with data._useNewPricing = false to use old pricing behavior.
New Constructor (preferred, introduced in 16.5)
  • constructor(Snapper snapper, str articleCode, str description, double basePrice, Double optionPriceSum, ...)
  • Accepts basePrice and optionPriceSum separately.
  • Marks Part with data._useNewPricing = true to activate new pricing behavior.
  • Double optionPriceSum Parameter
    • Nullable
    • If a value is provided → it is treated as the cached option price sum.
    • If null is provided → the option price sum will be generated on demand when required.
      • Recommended approach for most use cases.
      • See new pricing API section for generateOptionPriceSum() documentation
    • NOTE: cached option price sum value is removed in invalidateOptionPriceSum
Impact
  • Migrate to new constructor and split your values into basePrice and optionPriceSum.
  • All new core features and pricing rules will rely on the new constructor/pricing system.
  • The old constructor remains for backward compatibility
Example Old and New Constructor Usage
Old: new Part(snapper, "PART NUM", "PART DESC", listPrice=199.99);
New: new Part(snapper, "PART NUM", "PART DESC", basePrice=150, optionPriceSum=49.99);
New: new Part(snapper, "PART NUM", "PART DESC", basePrice=150, optionPriceSum=null);

Old Pricing API

The following functions are now part of the old pricing system. The new pricing API is introduced in v16.5 and documented in the next section. The old API will remain for backwards compatibility but is expected to be removed in a future version of CET.

extend public double customListPrice()
extend public Double upcharge()
extend public double customListPrice()
Old → New behavior

Old:

  • if applicable, returns PartProxy list price value
  • returns PartData cached list price value
  • overridable interface to customize list price value

New:

  • Same behavior within method.
Impact
  • Migrate to new pricing API and plan to remove all reliance on customListPrice()
  • See listPrice() and calculatedListPrice(..) documentation for more migration information
extend public Double upcharge()
Old → New behavior

Old:

  • upcharge() returns null.

New:

  • Replaced by optionPriceSum().
  • Still supported if useNewPricing() == false.
Impact
  • Migrate to new pricing API and plan to remove all reliance on upcharge()
  • See optionPriceSum(..) and calculatedListPrice(..) documentation for more migration information

New Pricing API (v16.5 and later)

The following functions were introduced as part of the new pricing system. They replace Old pricing methods that relied on a cached list price.

Added: final public bool useNewPricing() {}
Added: extend public double basePrice(bool includeChildren=false, Space space=null, bool translatePrice=true) {}
Added: extend public double optionPriceSum(bool includeChildren=false, Space space=null, bool translatePrice=true, bool invalidate=false) {}
Added: final public double calculatedListPrice(bool includeChildren=false, Space space=null) {}
Added: extend public Double generateOptionPriceSum() {}
Added: extend public void invalidateOptionPriceSum() {}
Added: extend public bool useNewPartPricing() {}

Indicates whether the new part pricing system should be used on the Part instance. An opt-in feature flag to the new pricing system.

Behavior
  • Returns cached value from PartData
  • Value is set during construction:
    • Using new constructor sets this value to true
    • Using old constructor sets this value to false
Impact
  • Acts as the feature flag for determining which pricing path is followed in constructors, listPrice, upcharge, basePrice, optionPriceSum, etc.
  • Developers should begin migrating logic to use base price + option price sum rather than raw list price.
Added: extend public double basePrice(bool includeChildren=false, Space space=null, bool translatePrice=true) {}
Parameters
  • bool includeChildren (optional)
    • default false
    • if true, recursively adds child part base prices to returned value
  • Space space (optional)
    • default null
    • utitlized for calling translatePrice
  • bool translatePrice (optional)
    • default true
    • if true, translates price value to current currency for space
Old → New behavior

Old:

  • basePrice() is implemented in cm.abstract.part.AbsPart
    • returns list price by default
    • does not utilize cached base price
    • does not utilize PartProxy
    • does not implement the core specials system
    • overridden in DsPart to return catalog base price

New:

  • basePrice() is implemented in cm.core.part.Part
    • if applicable, returns special amount if a PartSpecial is found
    • if applicable, returns PartProxy base price value
    • returns PartData cached base price
    • Supports:
      • Including child part base prices.
      • Optional currency translation via space.
      • Accounting for specials (PartSpecial).
Impact
  • Utilize the new constructor to set the cached basePrice for the Part
  • Replace usage of raw listPrice with basePrice where you need only the product’s inherent cost
Added: extend public double optionPriceSum(..)
Parameters
  • bool includeChildren (optional)
    • default false
    • if true, recursively adds child part option price sums to returned value
  • Space space (optional)
    • default null
    • utitlized for calling translatePrice
  • bool translatePrice (optional)
    • default true
    • if true, translates price value to current currency for space
  • bool invalidate (optional)
    • defaults false
    • if true, invalidates cached option price sum value, triggering regeneration
Old → New behavior

Old:

  • upcharge() is implemented in cm.abstract.part.AbsPart
    • returns null by default
    • does not utilize cached value
    • does not utilize PartProxy
    • does not utilize core specials system

New:

  • optionPriceSum() is implemented in cm.core.part.Part
    • if useNewPricing is false, returns upcharge()
    • if applicable, returns PartProxy option price sum value
    • returns PartData cached option price sum
      • if cached value is null (or invalidate=true), value is regenerated automatically with generateOptionPriceSum()
    • Supports:
      • Including child part option price sums.
      • Optional currency translation via space.
      • Accounting for specials (PartSpecial)
Impact
  • Replace calls to upcharge() with optionPriceSum()
  • If your code updates Part options dynamically, call invalidateOptionPriceSum() before recalculation.
Added: final public double calculatedListPrice(..)
Parameters
  • bool includeChildren (optional)
    • default false
    • if true, recursively adds child part list prices to returned value
  • Space space (optional)
    • default null
    • utilized for calling translatePrice
  • bool translatePrice (optional)
    • default true
    • if true, translates price value to current currency for space
  • bool invalidateOptionPriceSum (optional)
    • default false
    • if true, invalidates option price sum value for regeneration
Old → New behavior

Old:

  • customListPrice(..) is primary source of list price value
    • if applicable, returns PartProxy list price value
    • returns PartData cached list price value
    • overridable interface to customize list price value
    • Used by listPrice() when useNewPricing == false

New:

  • calculatedListPrice(..) is primary source of list price in new pricing model
    • no longer relies on a cached list price value
    • returns calculated list price value (basePrice(..) + optionPriceSum(..))
    • not an overridable interface
    • Used by listPrice() when useNewPricing == true
Impact
  • If your system overrides customListPrice(..) to customize list price, migrate this logic into:
    • basePrice(..) (for foundational Part price)
    • and/or optionPriceSum(..)/generateOptionPriceSum() (for option pricing logic).
  • listPrice() will automatically call calculatedListPrice(..) if useNewPricing is enabled.
Added: extend public Double generateOptionPriceSum()
Old → New behavior

Old:

  • Not applicable.

New:

  • Returns the newly generated option price total.
  • Default implementation returns null.
  • Extended Part classes should override this method to compute option pricing correctly for their system.
    • Overridden in ProdPart and DsPart
Impact
  • If you have custom option price logic, override generateOptionPriceSum() in your extended Part class.
  • Invalidate the cache (invalidateOptionPriceSum()) when option state changes to trigger regeneration.

Added: extend public void invalidateOptionPriceSum()

Old → New behavior

Old:

  • Not applicable.

New:

  • Invalidates cached optionPriceSum value to trigger regeneration
  • Call invalidateOptionPriceSum() whenever Part configuration changes (options, children, etc.)
Impact
  • Add invalidateOptionPriceSum() after any Part modifications that may affect option pricing.

New Part API

The following functions were added or changed in Part in v16.5. The new pricing model API is documented above and not included in this section.

translatePrice functions were modified to remove unused includeChildren parameter.

Added: extend public double totalBasePrice(bool includeChildren=false, Space space=null) {}
Added: extend public double totalOptionPriceSum(bool includeChildren=false, Space space=null) {}

Old: final public double translatePrice(double price, bool includeChildren=false, Space space=null) {
New: final public double translatePrice(double price, Space space=null) {

Old: final public double translatePrice(double price, currencyId currency, bool includeChildren=false, Space space=null) {
New: final public double translatePrice(double price, currencyId currency, Space space=null) {

New Lead Time Interface

As of 16.5, a new interface for lead time has been added to core Part. It's value is displayed in the new LeadTimeColumn implemented in cm/core/init/leadTimeColumn.cm.

In cm/core/part/part.cm

/**
 * Get lead time string.
 * Ex. 20 days, 3 weeks, etc.
 */
extend public str leadTime() {
	return "";
}

class WorldPart

Constructor Migration: List Price → Base Price + Option Price Sum

In v16.5, a new constructor has been introduced for WorldPart to support the new pricing model. Previously, WorldPart accepted a cached list price (listPrice) directly as a parameter. This behavior is now being phased out.

Old Constructor
  • constructor(World world, str articleCode, str description, double listPrice, ...)
  • Accepts a total listPrice value to cache.
  • See documentation in compile-time section for cm.core.part.Part for more info
New Constructor (preferred, introduced in 16.5)
  • constructor(World world, str articleCode, str description, double basePrice, Double optionPriceSum, ...)
  • Accepts basePrice and optionPriceSum separately for caching.
  • See documentation in compile-time section for cm.core.part.Part for more info
Impact
  • Migrate to new constructor and split your values into basePrice and optionPriceSum.
  • All new core features and pricing rules will rely on the new constructor/pricing system.
  • The old constructor remains for backward compatibility
Example Old and New Constructor Usage
Old: new WorldPart(snapper, "PART NUM", "PART DESC", listPrice=199.99);
New: new WorldPart(snapper, "PART NUM", "PART DESC", basePrice=150, optionPriceSum=49.99);
New: new WorldPart(snapper, "PART NUM", "PART DESC", basePrice=150, optionPriceSum=null);

class PartSpecialHolder

Added: public str[] specialKeys();

  • Stores an ordered sequence of special keys
  • Allows for tracking of order of insertion of added custom specials
Behavior
  • Appended to during put
  • Removed from during remove
  • Cleared during removeAll

class PartAnnotationHolder

See cm.core.part.attributes section in 16.5 New Features migration guide for new PartAnnotationHolder class documentation.

class PartData

New Interface

The following functions were added to PartData in v16.5. These functions were added in support of the new pricing model which is documented in the compile-time section for cm.core.part.Part.

// FUNCTIONS
New: extend public bool useNewPricing() {}
New: extend public double basePrice() : abstract {}
New: extend public void setBasePrice(double v) : abstract {}
New: extend public Double optionPriceSum() : abstract {}
New: extend public void setOptionPriceSum(Double v) : abstract {}

class PartAnnotation

See cm.core.part.attributes section in 16.5 New Features migration guide for new PartAnnotation class documentation.

class BasicPartData

Constructor Migration: List Price → Base Price + Option Price Sum

In v16.5, a new constructor has been introduced for BasicPartData to support the new pricing model. Previously, BasicPartData accepted a cached list price (listPrice) directly as a parameter. This behavior is now being phased out.

Old Constructor
  • constructor(Snapper snapper, str articleCode, str description, double inPrice=0, double outPrice=0, ...)
  • Accepts a total outPrice (list price) value to cache.
  • Marks _useNewPricing as false to use old pricing behavior.
New Constructor (preferred, introduced in 16.5)
  • constructor(Snapper snapper, str articleCode, str description, double basePrice=0, Double optionPriceSum=null, ...)
  • Accepts basePrice and optionPriceSum separately for caching.
  • Marks _useNewPricing as true to activate new pricing behavior.
  • Sets _outPrice field to (basePrice + optionPriceSum)
    • This field is unused in the new pricing model
  • Double optionPriceSum Parameter
    • See new pricing API section in cm.core.part.Part for documentation
Impact
  • Migrate to new constructor and split your values into basePrice and optionPriceSum.
  • All new core features and pricing rules will rely on the new constructor/pricing system.
  • The old constructor remains for backward compatibility
Example Old and New Constructor Usage
Old: new BasicPartData(snapper, "PART NUM", "PART DESC", outPrice=199.99);
New: new BasicPartData(snapper, "PART NUM", "PART DESC", basePrice=150, optionPriceSum=49.99);
New: new BasicPartData(snapper, "PART NUM", "PART DESC", basePrice=150, optionPriceSum=null);

New BasicPartData Interface

The following functions and fields were added to BasicPartData in v16.5. These functions were added in support of the new pricing model which is documented in the compile-time section for cm.core.part.Part.

// FIELDS
New: package bool _useNewPricing;
New: public double _basePrice;
New: public Double _optionPriceSum;

// OVERRIDDEN FUNCTIONS (from PartData)
New: public bool useNewPricing() {}
New: public double basePrice() {}
New: public void setBasePrice(double v) {}
New: public Double optionPriceSum() {}
New: public void setOptionPriceSum(Double v) {}

cm.core.part.query

class PartMakeSpecialDialog

Constructor change

The PartMakeSpecialDialog constructor has been changed to accept a new parameter label. The new parameter allows for customization of the dialogs label but the label is still defaulted to the title "Make/Change Special".

Old: public constructor(Window parent, PartSpecial original) {}
New: public constructor(Window parent, PartSpecial original, str label=$specialsDialog) {}

Added: extend public PartSpecial generateSpecial() {}

The helper method generateSpecial() has been introduced to streamline the process of creating PartSpecial objects from dialog input.

  • Encapsulates the logic for constructing a PartSpecial from dialog contents.
  • Uses current values from UI fields to populate the new object:
return PartSpecial(partNumTF.text,
                   descrTF.text,
                   priceReplaceRB.currentState > 0,
                   amountDF.value);

Added: extend public PartSpecial generateCustomSpecial(PartSpecial original) {}

The helper method generateCustomSpecial() has been introduced to streamline the process of creating custom PartSpecial objects from dialog input.

  • Currently returns null but provides a clear hook for future overrides.

In 16.5, the QueryDialogDataEnv has changed it's backing data structures, replacing the str->Part parts and int->str rowIDs maps with a single QueryRowData[] queryRows data sequence. Cell highlighting has also been introduced in the QueryDialog.

As a result, the following functions have been removed and added to QueryPartGridBuilderEnv.

Removed: public str rowIdentifier(Object data) {}
Added: public GridCell[] getRowCells(Object data) {}
Added: public Brush getCellBrush(Object data, int columnIdx) {}

class QueryDialogDataEnv

In 16.5, The QueryDialogDataEnv has changed it's backing data structures, replacing the str->Part parts and int->str rowIDs maps with a single QueryRowData[] queryRows data sequence.

Old vs. New Behavior

Old:

  • Relied on maps for matching grid row ids to Part instances
    • str->Part parts
    • int->str rowIDs
  • Challenges:
    • Heavily relied on keys and ID management for aligning UI with data
    • Prone to issues with duplicated keys/IDs which led to more complex handling
    • Introduction of custom specials unraveled this structure

New:

  • New atomic data structure to represent single rows in the grid (QueryRowData)
  • New single sequence in data env to hold rows (QueryRowData[] queryRows)
  • Improvements:
    • Direct access to row data using int indexes rather than str keys/ids
    • Single data structure to connect UI and data values
    • Ordered data sequence directly represents grid rows

As a result of this change, the following functions and fields were modified in the QueryDialogDataEnv interface.

// Modified data fields
Old: public str->Part parts;
Old: public int->str rowIDs;
New: public QueryRowData[] queryRows;

// Modified data field accessors/manipulation
Old: extend public Part[] setParts(Part[] value) {}
Old: extend public int->str setRowIDs(int->str value) {}
Old: extend public void buildParts() {}
New: extend public void buildQueryRowData(Part[] parts=null) {}

// Modified to new data structure (str id -> int row)
Old: extend public PartSpecial getSpecial(str id) {} 
New: extend public PartSpecial getSpecial(int row) {}

Old: extend public void putSpecial(str id, PartSpecial special) {}
New: extend public void putSpecial(int row, PartSpecial special) {}

Old: extend public void removeSpecial(str id) {}
New: extend public void removeSpecial(int row) {}

// Added helper functions
New: extend public void refreshQueryRowData() {}
New: extend public QueryRowData createPartRowData(Part part) {}
New: extend public bool contains(PropObj propObj) {}
New: extend public bool any() {}
New: extend public bool empty() {}
New: extend public QueryRowData getRowData(int row) {}

NOTE: Things labelled as Old were removed interface and those labeled as New were added. The Old/New labels are utilized to clearly demonstrate the migration of changes.

In 16.5, the QueryDialogDataEnv has changed it's backing data structures, replacing the str->Part parts and int->str rowIDs maps with a single QueryRowData[] queryRows data sequence.

As a result, the extended type QueryPartRowData has been created to represent a core Part query dialog row.

/**
 * QueryPartRowData
 * 
 * This class is responsible for managing 
 * data for a Part row in a QueryDialog.
 */
public class QueryPartRowData extends QueryRowData {
    
    /**
     * Get the Part instance associated with this row.
     * @return The Part object associated with this row
     */
    extend public Part part() { ... }
    
    
    /**
     * Get the special associated with this row.
     * @return The PartSpecial object associated with this row
     */
    public PartSpecial getSpecial() { ... }
    
    
    /**
     * Assign a special to this row.
     * @special The PartSpecial object to associate with this row
     */
    public void putSpecial(PartSpecial special) { ... }
    
    
    /**
     * Remove the special from this row.
     */
    public void removeSpecial() { ... }
}

class QueryDialogBehavior

Data Backend Changes

In 16.5, The QueryDialogDataEnv has changed it's backing data structures, replacing the str->Part parts and int->str rowIDs maps with a single QueryRowData[] queryRows data sequence. View more documentation about this change in the section for cm.core.part.query.QueryDialogDataEnv.

The following functions in the QueryDialogBehavior have been updated to reflect the new index-based data retrieval versus the old key/id based retrieval. They perform the same purposes with the only notable change being in buildGridWindow which is documented further below.

Old: extend public int->str buildGridWindow(GridWindow grid, Part[] parts) {}
New: extend public void buildGridWindow(GridWindow grid, Object rows) {}

Old: extend public PartSpecial getSpecial(QueryDialog dialog, str id) {}
New: extend public PartSpecial getSpecial(QueryDialog dialog, int row) {}

Old: extend public void putSpecial(QueryDialog dialog, str id, PartSpecial special) {}
New: extend public void putSpecial(QueryDialog dialog, int row, PartSpecial special) {}

Old: extend public void removeSpecial(QueryDialog dialog, str id) {}
New: extend public void removeSpecial(QueryDialog dialog, int row) {}

Old: extend public GridCell[] getRowCells(QueryDialog dialog, str id) {}
New: extend public GridCell[] getRowCells(QueryDialog dialog, int row) {}

Old: extend public GridCell getCell(QueryDialog dialog, str columnLabel, str rowID) {}
New: extend public GridCell getCell(QueryDialog dialog, str columnLabel, int row) {}

Old: extend public DialogWindow getSpecialDialog(QueryDialog dialog, str id) {}
New: extend public DialogWindow getSpecialDialog(QueryDialog dialog, int row) {}
Changed: extend public int->str buildGridWindow(GridWindow grid, Part[] parts) {}

The buildGridWindow function no longer takes in a Part sequence as a parameter and no longer returns an int->str row-index to row-id map. It now accepts a generic Object type for its row building. It's return type has changed to void with the reverted need for row IDs.

Old vs. New Behavior

Old:

  • Accepted Part[] parts as a parameter
  • Built rows directly from Parts sequence
  • Returned int->str map of row-indexes to row-ids for caching on QueryDialogDataEnv

New:

  • Accepts an Object type for a row data parameter
  • Builds rows using GridBuilders generic populateGridWindow function which takes in an Object
  • Returns nothing anymore as row-ids are no longer cached on QueryDialogDataEnv
Impact
  • Developers wanting to manage their grid building should override GridBuilder and it's populateGridWindow interface
    • Supply the customized builder to the QueryDialogBehavior on your PropObj

Other QueryDialogBehavior Changes

The following functions have been modified, added to, or removed from QueryDialogBehavior in v16.5.

Added: extend public QueryRowData[] getQueryRows(QueryDialog dialog) {}
Added: extend public QueryRowData getRowData(QueryDialog dialog, int row) {}
Added: extend public Part getPart(QueryDialog dialog, int row) {}
Added: extend public void openCustomSpecialDialog(QueryDialog dialog) {}
Added: extend public DialogWindow getCustomSpecialDialog(QueryDialog dialog, int row) {}

Removed: extend public bool containsSpecial(QueryDialog dialog) {}
Removed: extend public int getRowIndex(QueryDialog dialog, str id) {}

Added: extend public QueryRowData[] getQueryRows(QueryDialog dialog) {}

  • Returns all QueryRowDatas in the dialogs QueryDialogDataEnv

Added: extend public QueryRowData getRowData(QueryDialog dialog, int row) {}

  • Returns the QueryRowData representing the row at the int row index

Added: extend public Part getPart(QueryDialog dialog, int row) {}

  • Returns the Part representing the row at the int row index

Added: extend public void openCustomSpecialDialog(QueryDialog dialog) {}

  • Opens a dialog (PartMakeSpecialDialog) for generating a custom special representing the row at the selected row index

Added: extend public DialogWindow getCustomSpecialDialog(QueryDialog dialog, int row) {}

  • Gets the dialog (PartMakeSpecialDialog) for generating a custom special representing the row at the selected row index

Removed: extend public bool containsSpecial(QueryDialog dialog) {}

  • This function was removed with the removal of the old data structure in QueryDialogDataEnv
  • See documentation in the above "New Data Backend" section and/or the compile-time section for cm.core.part.query.QueryDialogDataEnv for more info.

Removed: extend public int getRowIndex(QueryDialog dialog, str id) {}

  • This function was removed with the removal of the old data structure in QueryDialogDataEnv
  • See documentation in the above "New Data Backend" section and/or the compile-time section for cm.core.part.query.QueryDialogDataEnv for more info.

class QueryRowData

In 16.5, the QueryDialogDataEnv has changed it's backing data structures, replacing the str->Part parts and int->str rowIDs maps with a single QueryRowData[] queryRows data sequence. As a result, the abstract class QueryRowData has been created to represent a generic query dialog row.

  • Carryies current instance of a row's data (Part, SpecOption, etc)
  • Manipulates specials for the row's data type

Interface

/**
 * QueryRowData
 * 
 * This abstract class is responsible for managing 
 * data for a row in a QueryDialog.
 */
public class QueryRowData : abstract {

    /**
     * ID for this row data instance.
     */
    public str id;
    
    /**
     * The data object associated with this row.
     */
    public Object data : copy=reference;

    
    /**
     * Constructor.
     * @id ID for this row
     * @data The data object to associate with this row
     */
    public constructor(str id, Object data) { ... }


    /**
     * Get the special associated with this row.
     * Override to get specific special handling logic.
     * @return The PartSpecial object associated with this row
     */
    extend public PartSpecial getSpecial() : abstract { }


    /**
     * Assign a special to this row.
     * Override to get specific special handling logic.
     * @special The PartSpecial object to associate with this row
     */
    extend public void putSpecial(PartSpecial special) : abstract { }


    /**
     * Remove the special from this row.
     * Override to get specific special handling logic.
     */
    extend public void removeSpecial() : abstract { }
}

class QueryDialog

Data Backend Changes

In 16.5, The QueryDialogDataEnv has changed it's backing data structures, replacing the str->Part parts and int->str rowIDs maps with a single QueryRowData[] queryRows data sequence. View more documentation about this change in the section for cm.core.part.query.QueryDialogDataEnv.

The following functions in the QueryDialog have been updated, added, or removed to reflect the new index-based data retrieval versus the old key/id based retrieval.

Old: extend public str selectedRowID() {}
New: extend public int selectedRow() {}

Old: extend public PartSpecial getSpecial(str id) {}
New: extend public PartSpecial getSpecial(int row) {}

Old: extend public void putSpecial(str id, PartSpecial special) {}
New: extend public void putSpecial(int row, PartSpecial special) {}

Old: extend public void removeSpecial(str id) {}
New: extend public void removeSpecial(int row) {}

Removed: extend public str->Part parts() {}
Removed: extend public int->str rowIDs() {}
Added: extend public QueryRowData selectedRowData() {}

Other QueryDialog Changes

The following functions have been added and removed from QueryDialog in v16.5.

Added: extend public bool any() {}
Added: extend public bool empty() {}
Added: extend public bool contains(PropObj propObj) {}
Added: public bool keyCode(Window originator, KeyCode keyCode) {}

Removed: extend public bool containsSpecial() {}
Added: extend public bool any() {}
  • Checks if the QueryDialog contains any backing data
  • A quick way to see if the QueryDialog has any data
Behavior
  • Calls any() on the dialogs QueryDialogDataEnv env
  • Returns true if data contains any QueryDataRows; false if not
Added: extend public bool empty() {}
  • Checks if the QueryDialog contains any backing data
  • A quick way to see if the QueryDialog has no data
Behavior
  • Calls empty() on the dialogs QueryDialogDataEnv env
  • Returns false if data contains any QueryDataRows; false if not
Added: extend public bool contains(PropObj propObj) {}
  • Checks if the QueryDialog contains the passed in propObj in it's backing data
Behavior
  • Calls contains(..) on the dialogs QueryDialogDataEnv env
  • See documentation in the compile-time section for cm.core.part.query.QueryDialogDataEnv for more info
Added: public bool keyCode(Window originator, KeyCode keyCode) {}
  • A new override of keyCode was added in QueryDialog to intercept Ctrl+A events originating from a QueryDialog.
  • Previously, Ctrl+A would bubble up to CoreAppWindow and trigger a selectAll action on snapper elements, which was not desirable.
Behavior
  • Intercepts the Ctrl+A key combination.
    • If the event originates from a GridWindow, it returns true immediately to prevent parent handling
    • Otherwise, the event is forwarded to the query dialogs GridWindow via gridWindow().?key(CTRL_A)
  • All other key events are passed to the superclass via super(..)

cm.core.propsScheme

initFromPropsScheme cleanup

The following interfaces have been consolidated, there should be no necessary changes.

Old: public void initFromPropsScheme(PropObj obj) : deprecated {
New: public void initFromPropsScheme(PropObj obj, str systemKey=null, World world=null) {

PropsSchemeQPInfo cleanup

We have made str systemKey an optional argument and removed the World w=null argument.

Old: public PropsSchemeSpecificQPInfo propsSchemeQPInfo(CoreObject obj, World w=null) {
Old: public PropsSchemeSpecificQPInfo propsSchemeQPInfo(CoreObject obj, str systemKey, World w=null) {
New: public PropsSchemeSpecificQPInfo propsSchemeQPInfo(CoreObject obj, str systemKey=null) {

PropsSchemeFilterButton

Following the interface change introduced for BrushHoverButton, the constructor for PropsSchemeFilterButton has been updated.

Old: public constructor(Window parent,
                        Brush stdBrush,
                        Brush hoverBrush,
                        Brush mouseDownBrush,
                        // inherited key arguments
                        Font font=controlFont,
                        FrameStyle frameStyle=noFrame,
                        frame3DState frameState=frameStateUp,
                        pointI pos=(0, 0),
                        sizeI size=sizeI(-1, -1),
                        pointI margins=(6, 4),
                        alignment align=middle,
                        str key=null,
                        str label="",
                        color labelColor=color(0, 0, 0),
                        color pressedLabelColor=color(colorType.none),
                        alignment textSide=undefinedAlignment,
                        Image image=null,
                        Image disabledImage=null,
                        color color=nocolor,
                        bool roundedCorners=false,
                        function(Control button) callback=null,
                        SrcRef src=#:src) {

New: public constructor(Window parent,
                        Brush stdBrush,
                        Brush hoverBrush,
                        Brush mouseDownBrush,
                        // inherited key arguments
                        Font font=controlFont,
                        FrameStyle frameStyle=noFrame,
                        frame3DState frameState=frameStateUp,
                        pointI pos=(0, 0),
                        sizeI size=sizeI(-1, -1),
                        pointI margins=(6, 4),
                        alignment align=middle,
                        str key=null,
                        str label="",
                        color labelColor=color(0, 0, 0),
                        color pressedLabelColor=color(colorType.none),
                        alignment textSide=undefinedAlignment,
                        Image image=null,
                        Image disabledImage=null,
                        color color=nocolor,
                        bool roundedCorners=false,
                        bool showUnderline=false, // Added
                        function(Control button) callback=null,
                        SrcRef src=#:src) {

PropsSchemeDialog

We have optimized props scheme dialog by eliminating unnecessary nested SubWindow in cards, schemeBarSub, barsAndOptions, and applyWins. The following methods have been removed as they are now unnecessary:

Removed: extend public void layoutApply(Window reference=null) {}
Removed: extend public void layoutSelection() {}

Previously, these methods were used to adjust margins caused by the presence of the now-removed applyWindow sub-window.

psCardWindow

The following methods have been removed as redundant window resizing operations are no longer required in these methods:

Removed: public void ancestorResizeEnd() {}
Removed: public void parentClientBoundChanged() {}

They will now inherit the behaviours from Card instead.

cm.core.selection

SelectionFloatingDialog

A small addition of a default argument str loc=callerLoc() to the constructor.

Old: public constructor(Window parent,
                        Font font=systemFont(),
                        Brush brush=null,
                        str label="Dialog",
                        bool initVisible=true,
                        bool noBorder=false) : deprecated {
New: public constructor(Window parent,
                        Font font=systemFont(),
                        Brush brush=null,
                        str label="Dialog",
                        bool initVisible=true,
                        bool noBorder=false,
                        str loc=callerLoc()) { // Added

cm.core.toolbox

TBInfo changes

The class field lastSelectedTab has been removed and is replaced with lastSelectedTabKey. The decision to store the last selected tab's key instead of the index position is to allow exact string matching when remembering and restoring the last selected toolbox tab, further improving consistency when loading the last selected toolbox tab.

Removed: public int lastSelectedTab;
Added: public str lastSelectedTabKey;

MessageWindow

The MessageWindow class has been moved from cm.core.toolbox to cm.win.

Toolbox

The cm.win function endProgress was accidentally overriden by Toolbox in 16.0, this has been removed.

Removed:  final public void endProgress() {

ToolboxButtonPainter

New argument UIHint hint has been added.

Old: public constructor(ComboTextPainter victim, str->Object styles) : deprecated {
New: public constructor(ComboTextPainter victim, str->Object styles, UIHint hint) {

LazySnapper3DMemoryButton

The method convertToBitmap has been removed as it is no longer necessary to convert a DibImage into a MemoryImage.

Removed: extend public void convertToBitmap(DibImage dib, sizeI dim) {

cm.core.user

class UserPart

Constructor Migration: List Price → Base Price + Option Price Sum

In v16.5, a new constructor has been introduced for UserPart to support the new pricing model. Previously, UserPart accepted a cached list price (listPrice) directly as a parameter. This behavior is now being phased out.

Old Constructor
  • constructor(World world, ..., double listPrice, ...)
  • Accepts a total listPrice value to cache.
  • See documentation in compile-time section for cm.core.part.Part for more info
New Constructor (preferred, introduced in 16.5)
  • constructor(World world, ..., double basePrice, Double optionPriceSum, ...)
  • Accepts basePrice and optionPriceSum separately for caching.
  • See documentation in compile-time section for cm.core.part.Part for more info
Impact
  • Migrate to new constructor and split your values into basePrice and optionPriceSum.
  • All new core features and pricing rules will rely on the new constructor/pricing system.
  • The old constructor remains for backward compatibility

cm.core.visibility.categorize

Remove: public class PrevCatChecked {
Old: public class CategoryUndoOp {
New: package class CategoryUndoOp {
Old: public class AddCategoryUndoOp {
New: package class AddCategoryUndoOp {
Old: public class RemoveCategoryUndoOp {
New: package class RemoveCategoryUndoOp {

class CategoryUndoOp

Removed: public PrevCatChecked{} theDescendants : stream=null;
Old: public symbol cat;
New: private symbol cat;
Old: public Snapper{} taggedSnappers : stream=null;
New: private Snapper{} taggedSnappers() : stream=null;
Old: public constructor(symbol cat, Snapper{} snappers, PrevCatChecked{} theDescendants = null) {
New: public constructor(symbol cat, Snapper{} snappers) {

cm.core3D

gMaterial3DLegacy.cm

MemoryImage to be casted to DibImage and destroyed explicitly to reduce GDI Object usage.

Old: public MemoryImage loadOldGMaterial3DThumbnail(Url url, gmThumbnailType thumbnailType=gmThumbnailType.flat) {
New: public DibImage loadOldGMaterial3DThumbnail(Url url, gmThumbnailType thumbnailType=gmThumbnailType.flat) {

gMaterial3DHelpers.cm

Since getThumbnail(memoryStream) return a DibImage now, return it as a DibImage in loadDexGMThumbnail().

Old: public Image loadDexGMThumbnail(Url url, gmThumbnailType thumbnailType=gmThumbnailType.flat) {
New: public DibImage loadDexGMThumbnail(Url url, gmThumbnailType thumbnailType=gmThumbnailType.flat) {

Match loadOldGMaterial3DThumbnail() and loadDexGMThumbnail() change to return DibImage.

Old: public Image loadGMaterial3DThumbnail(Url url, gmThumbnailType thumbnailType=gmThumbnailType.flat) {
New: public DibImage loadGMaterial3DThumbnail(Url url, gmThumbnailType thumbnailType=gmThumbnailType.flat) {

Text3D changes

The following new fields fontWeight wght, bool italic, and bool underline have been added. As such, a constructor has been updated to set these fields.

public class Text3D extends Primitive3D {
    Old: public constructor(str text, float height, float thickness, str fontFace, LayerExpr layer=null) {
    New: public constructor(str text, float height, float thickness, str fontFace, fontWeight wght=fontWeight.normal, bool italic=false, bool underline=false, LayerExpr layer=null) {
}

cm.format.dwg

Cleanup of constructors of Dwg entites. Remove redundant construcors and replace differences with default arguments.

DwgArc

Old: public constructor(point2D center, double startAngle, double endAngle, double radius, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c)
Old: public constructor(point2D center, double startAngle, double endAngle, double radius, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c, double lineWeight)
New: public constructor(point2D center, double startAngle, double endAngle, double radius, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c, double lineWeight=defaultLineWeight)

DwgFace

Old: public constructor(point p0, point p1, point p2, bool i0, bool i1, bool i2, color c, DwgLayer layer, DwgBlock block, DwgLineType linetype)
Old: public constructor(point p0, point p1, point p2, bool i0, bool i1, bool i2, color c, DwgLayer layer, DwgBlock block, DwgLineType linetype, double lineWeight)
New: public constructor(point p0, point p1, point p2, bool i0, bool i1, bool i2, color c, DwgLayer layer, DwgBlock block, DwgLineType linetype, double lineWeight=defaultLineWeight, COLORTYPE colorType=cLayer)

DwgLine

Old: public constructor(point start, point end, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c)
Old: public constructor(point start, point end, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c, double lineWeight)
New: public constructor(point start, point end, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c, double lineWeight=defaultLineWeight)

DwgPoint

Old: public constructor(point position, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c)
Old: public constructor(point position, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c, double lineWeigh)
Old: public constructor(point position, DwgLayer layer, DwgLineType linetype, DwgBlock block, COLORTYPE colorType, color c, int16 colorIndex, double lineWeight)
New: public constructor(point position, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c, COLORTYPE colorType=cLayer, int16 colorIndex=-1, double lineWeight=defaultLineWeight)

DwgPolyFaceMesh

Old: public constructor(AMultiMesh mesh, color c, DwgLayer layer, DwgBlock block, DwgLineType linetype, bool[] visibleEdges=null)
Old: public constructor(AMultiMesh mesh, color c, DwgLayer layer, DwgBlock block, DwgLineType linetype, double lineWeight, bool[] visibleEdges=null)
New: public constructor(AMultiMesh mesh, color c, DwgLayer layer, DwgBlock block, DwgLineType linetype, double lineWeight=defaultLineWeight, COLORTYPE colorType = cLayer, bool[] visibleEdges=null)

DwgPolyLine

Old: public constructor(point2D[] points, bool closed, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c)
Old: public constructor(point2D[] points, bool closed, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c, double lineWeight)
Old: public constructor(point2D[] points, bool closed, DwgLayer layer, DwgLineType linetype, DwgBlock block, COLORTYPE colorType, color c, int16 colorIndex, double lineWeight)
New: public constructor(point2D[] points, bool closed, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c, COLORTYPE colorType=cLayer, int16 colorIndex=-1, double lineWeight=defaultLineWeight)

DwgRasterImage

Old: public constructor(str name, str fileName, point origin, vector width, vector height, sizeI pixelSize, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c)
Old: public constructor(str name, str fileName, point origin, vector width, vector height, sizeI pixelSize, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c, double lineWeight)
New: public constructor(str name, str fileName, point origin, vector width, vector height, sizeI pixelSize, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c, double lineWeight=defaultLineWeight)

DwgSolid

Old: public constructor(point2D[] points, double thickness, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c)
Old: public constructor(point2D[] points, double thickness, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c, double lineWeight)
New: public constructor(point2D[] points, double thickness, DwgLayer layer, DwgLineType linetype, DwgBlock block, color c, COLORTYPE colorType=cLayer, double lineWeight=defaultLineWeight)

load.cm

Removed redundant loadDwg function

Removed: public DwgRoot loadDwg(Url path, DwgImportEnv importEnv=null, bool verbose=false)

cm.network.cbb

# CBBClient
Changed:
- final public void showLoginFailNotification() 
+ final public void showLoginFailNotification(rtoutcome reason, str errorType) 

cm.std.draw3D

Draw3DMeasureBase changes

The public field color currentFontColor has been removed and a new private field UserTextStyle _tStyle has been added. The old currentFontColor data is now stored inside the new field _tStyle. New methods color currentFontColor() and color currentFontColor=(color c) have been added so the interface of setting currentFontColor remains the same. By default, _tStyle will follow the style of Dimension text under the tools tab. However for existing 3D dimension from old drawing, user will still be seeing the previous style which always Arial with 50mm font height.

public class Draw3DMeasureBase extends DrawSnapper : abstract {
    Removed: public color currentFontColor;
    New: private UserTextStyle _tStyle;

New:
    /**
     * Current font color (get).
     */
    extend public color currentFontColor() {
        if (tStyle) return tStyle.fontColor();
        return black;
    }


New:
    /**
     * Current font color (set).
     */
    extend public color currentFontColor=(color c) {
        if (tStyle) {
            if (!tStyle.followedStyle) {
                tStyle = tStyle.editedCopy().UserTextStyle;
            }
            tStyle.setFontColor(c);
        }
        return currentFontColor();
    }
}

cm.test.cmunit.testInstructions

class GetTreeViewItemAtIndexInstruction

This class has been removed and can be replaced with the new GetTreeViewItemsInstruction.

Removed: class GetTreeViewItemAtIndexInstruction

class GetTreeViewItemInstruction

This class has been removed and can be replaced with the new GetTreeViewItemsInstruction.

Removed: class GetTreeViewItemInstruction

class InsertSnapperInstruction

Changed the type of the skipSnap constructor argument from bool to Bool. When assigned to null, userSnapDisable will not be overridden and instead use its global value during insert.

Old: public constructor(Snapper snapper, line[] mouseLines, str outputKey=null, bool skipSnap=false, SrcRef src=#:src) {
New: public constructor(Snapper snapper, line[] mouseLines, str outputKey=null, Bool skipSnap=null, SrcRef src=#:src) {
Old: public constructor(SnapperSpawner spawner, line[] mouseLines, str outputKey=null, bool skipSnap=false, SrcRef src=#:src) {
New: public constructor(SnapperSpawner spawner, line[] mouseLines, str outputKey=null, Bool skipSnap=null, SrcRef src=#:src) {
Old: public constructor(Snapper snapper, point2D[] pos, str outputKey=null, bool skipSnap=false, SrcRef src=#:src) {
New: public constructor(Snapper snapper, point2D[] pos, str outputKey=null, Bool skipSnap=null, SrcRef src=#:src) {
Old: public constructor(SnapperSpawner spawner, point2D[] pos, str outputKey=null, bool skipSnap=false, SrcRef src=#:src) {
New: public constructor(SnapperSpawner spawner, point2D[] pos, str outputKey=null, Bool skipSnap=null, SrcRef src=#:src) {
Old: public constructor(Snapper snapper, line mouseLine, str outputKey=null, bool skipSnap=false, SrcRef src=#:src) {
New: public constructor(Snapper snapper, line mouseLine, str outputKey=null, Bool skipSnap=null, SrcRef src=#:src) {
Old: public constructor(SnapperSpawner spawner, line mouseLine, str outputKey=null, bool skipSnap=false, SrcRef src=#:src) {
New: public constructor(SnapperSpawner spawner, line mouseLine, str outputKey=null, Bool skipSnap=null, SrcRef src=#:src) {
Old: public constructor(Snapper snapper, point2D pos=point2D(), str outputKey=null, bool skipSnap=false, SrcRef src=#:src) {
New: public constructor(Snapper snapper, point2D pos=point2D(), str outputKey=null, Bool skipSnap=null, SrcRef src=#:src) {
Old: public constructor(SnapperSpawner spawner, point2D pos=point2D(), str outputKey=null, bool skipSnap=false, SrcRef src=#:src) {
New: public constructor(SnapperSpawner spawner, point2D pos=point2D(), str outputKey=null, Bool skipSnap=null, SrcRef src=#:src) {
Old: public constructor(SnapperSpawner spawner, str outputKey, bool skipSnap=false, SrcRef src=#:src) {
New: public constructor(SnapperSpawner spawner, str outputKey, Bool skipSnap=null, SrcRef src=#:src) {

class PutSnappersInBlockInstruction -> CreateBlockInstruction

This instruction has been renamed CreateBlockInstruction.

Old: public class PutSnappersInBlockInstruction extends TestInstruction {
New: public class CreateBlockInstruction extends TestInstruction {

class ValidateTreeViewItemIsCheckedInstruction -> ValidateCheckboxStateInstruction

This instruction now handles TriStateCheckBox as well and has therefore been given a more general name.

Old: public class ValidateTreeViewItemIsCheckedInstruction extends ValidateInstruction {
New: public class ValidateCheckboxStateInstruction extends ValidateInstruction {

Old: public constructor(str itemKey, bool expectedChecked, SrcRef src=#:src) { New: public constructor(str objectKey, bool expectedChecked, SrcRef src=#:src) {

Old: public constructor(str itemKey, extent expectedState, SrcRef src=#:src) {
New: public constructor(str objectKey, extent expectedState, SrcRef src=#:src) {

class ValidateTreeViewItemKeyInstruction

This class has been removed. As a replacement, use GetFieldValueInstruction to read the "key"-field of the tree view item, followed by ValidateValueInstruction.

Removed: class ValidateTreeViewItemKeyInstruction

cm.win

SolidColorBrush

The field c in SolidColorBrush has now been changed to public readable to prevent alteration of existing brushes.

Old: public color c;
New: package color c : public readable;

The return type of brush cache function to get a cached solid color brush is now more specific.

Old: public Brush solidColorBrush(color c, bool use=false) {
New: public SolidColorBrush solidColorBrush(color c, bool use=false) {

Old: public Brush solidColorBrush(int r, int g, int b) {
New: public SolidColorBrush solidColorBrush(int r, int g, int b) {

Predefined solid brushes in cm/win/brush.cm now have their types changed to SolidColorBrush.

Old: public const Brush whiteBrush = SolidColorBrush(color(255, 255, 255));
public const SolidColorBrush whiteBrush = solidColorBrush(color(255, 255, 255));

GradientBrush

For both GradientHBrush and GradientVBrush, the field c2 has been changed to public readable.

Old: public color c2;
New: package color c2 : public readable;

ScrollableGridWindow

Added a new argument faceliftScrollBars=false in ScrollableGridWindow constructor.

MessageWindow

The MessageWindow class has been moved from cm.core.toolbox to cm.win.

Instead of passing a sizeI size in the constructor argument, only int width is passed instead.

Old: public constructor(Window parent,
		       str message,
		       Image image,
		       str key=null,
		       sizeI size=(0, 0),
		       int internalMargin=7,
		       Brush brush=ultraLightGrayBrush,
		       FrameStyle frameStyle=lightGrayPenFrame,
		       color textColor=black,
		       color linkColor=primary600,
		       color linkHoverColor=primary600,
		       int textSize=12,
		       str fontFace=null,
		       function(Control button, str key):bool linkCallback=null,
		       SrcRef src=#:src) {
New: public constructor(Window parent,
		       str message,
		       Image image,
		       str key=null,
		       int width=0,
		       int internalMargin=7,
		       Brush brush=ultraLightGrayBrush,
		       FrameStyle frameStyle=lightGrayPenFrame,
		       color textColor=black,
		       color linkColor=primary600,
		       color linkHoverColor=primary600,
		       int textSize=12,
		       str fontFace=null,
		       function(Control button, str key):bool linkCallback=null,
		       SrcRef src=#:src) {

BrushHoverButton

The constructor for BrushHoverButton has been updated to allow an optional configuration of underlining the button's label text when a button is hovered. This should facilitate a smoother integration for button styles, or more specifically, button states that adhere to CET's design system.

Old: public constructor(Window parent,
                        Brush stdBrush,
                        Brush hoverBrush,
                        Brush mouseDownBrush,
                        // inherited key arguments
                        Font font=controlFont,
                        FrameStyle frameStyle=noFrame,
                        frame3DState frameState=frameStateUp,
                        pointI pos=(0, 0),
                        sizeI size=sizeI(-1, -1),
                        pointI margins=(6, 4),
                        alignment align=middle,
                        str key=null,
                        str label="",
                        color labelColor=color(0, 0, 0),
                        color pressedLabelColor=color(colorType.none),
                        alignment textSide=undefinedAlignment,
                        Image image=null,
                        Image disabledImage=null,
                        color color=nocolor,
                        bool roundedCorners=false,
                        function(Control button) callback=null,
                        SrcRef src=#:src) {

New: public constructor(Window parent,
                        Brush stdBrush,
                        Brush hoverBrush,
                        Brush mouseDownBrush,
                        // inherited key arguments
                        Font font=controlFont,
                        FrameStyle frameStyle=noFrame,
                        frame3DState frameState=frameStateUp,
                        pointI pos=(0, 0),
                        sizeI size=sizeI(-1, -1),
                        pointI margins=(6, 4),
                        alignment align=middle,
                        str key=null,
                        str label="",
                        color labelColor=color(0, 0, 0),
                        color pressedLabelColor=color(colorType.none),
                        alignment textSide=undefinedAlignment,
                        Image image=null,
                        Image disabledImage=null,
                        color color=nocolor,
                        bool roundedCorners=false,
                        bool showUnderline=false, // Added
                        function(Control button) callback=null,
                        SrcRef src=#:src) {

As part of the interface change introduced for BrushHoverButton, some classes that extend from it will also be affected.

See other affected changes in:

  • cm.core.propsScheme.propsSchemeFilterButton
  • cm.win.frameStyleHoverButton
  • cm.win.progressButton

FaceliftSearchField

The constructor for FaceliftSearchField has been updated to accept an optional search icon that can be used to override the search field's icon where necessary.

Old: public constructor(Window parent,
                        // inherited key arguments
                        Font font=inputFieldFont,
                        Brush brush=whiteBrush,
                        FrameStyle frameStyle=faceliftStdFrame,
                        frame3DState frameState=frameStateDown,
                        pointI pos=(0, 0),
                        sizeI size=(0, 0),
                        pointI margins=(8, 8),
                        function(Control button) posChangedCallback=null,
                        function(Control button) callback=null,
                        function(Control button) escapeKeyCallback=null,
                        function(Control button) lostFocusCallback=null,
                        // extended
                        function(Control button) enterKeyCallback=null,
                        str key=null,
                        bool absFontH=false,
                        str promptText=null,
                        SrcRef src=#:src) {

New: public constructor(Window parent,
                        // inherited key arguments
                        Font font=inputFieldFont,
                        Brush brush=whiteBrush,
                        FrameStyle frameStyle=faceliftStdFrame,
                        frame3DState frameState=frameStateDown,
                        pointI pos=(0, 0),
                        sizeI size=(0, 0),
                        pointI margins=(8, 8),
                        Image searchIcon=icon("facelift2023/search"), // Added
                        function(Control button) posChangedCallback=null,
                        function(Control button) callback=null,
                        function(Control button) escapeKeyCallback=null,
                        function(Control button) lostFocusCallback=null,
                        // extended
                        function(Control button) enterKeyCallback=null,
                        str key=null,
                        bool absFontH=false,
                        str promptText=null,
                        SrcRef src=#:src) {

FrameStyleHoverButton

Following the interface change introduced for BrushHoverButton, the constructor for FrameStyleHoverButton has been updated.

Old: public constructor(Window parent,
                        FrameStyle mouseOverFrameStyle|,
                        // inherited key arguments
                        Brush stdBrush,
                        Brush hoverBrush,
                        Brush mouseDownBrush,
                        Font font=controlFont,
                        FrameStyle frameStyle=noFrame,
                        frame3DState frameState=frameStateUp,
                        pointI pos=(0, 0),
                        sizeI size=sizeI(-1, -1),
                        pointI margins=(6, 4),
                        alignment align=middle,
                        str key=null,
                        str label="",
                        color labelColor=color(0, 0, 0),
                        color pressedLabelColor=color(colorType.none),
                        alignment textSide=undefinedAlignment,
                        Image image=null,
                        Image disabledImage=null,
                        color color=nocolor,
                        bool roundedCorners=false,
                        function(Control button) callback=null,
                        SrcRef src=#:src) {

New: public constructor(Window parent,
                        FrameStyle mouseOverFrameStyle|,
                        // inherited key arguments
                        Brush stdBrush,
                        Brush hoverBrush,
                        Brush mouseDownBrush,
                        Font font=controlFont,
                        FrameStyle frameStyle=noFrame,
                        frame3DState frameState=frameStateUp,
                        pointI pos=(0, 0),
                        sizeI size=sizeI(-1, -1),
                        pointI margins=(6, 4),
                        alignment align=middle,
                        str key=null,
                        str label="",
                        color labelColor=color(0, 0, 0),
                        color pressedLabelColor=color(colorType.none),
                        alignment textSide=undefinedAlignment,
                        Image image=null,
                        Image disabledImage=null,
                        color color=nocolor,
                        bool roundedCorners=false,
                        bool showUnderline=false, // Added
                        function(Control button) callback=null,
                        SrcRef src=#:src) {

ProgressButton

Following the interface change introduced for BrushHoverButton, the constructor for ProgressButton has been updated.

Old: public constructor(Window parent,
                        Brush stdBrush,
                        Brush hoverBrush,
                        Brush mouseDownBrush,
                        // inherited key arguments
                        Font font=controlFont,
                        FrameStyle frameStyle=noFrame,
                        frame3DState frameState=frameStateUp,
                        pointI pos=(0, 0),
                        sizeI size=sizeI(-1, -1),
                        pointI margins=(6, 4),
                        alignment align=middle,
                        str key=null,
                        str label="",
                        color labelColor=color(0, 0, 0),
                        color pressedLabelColor=color(colorType.none),
                        alignment textSide=undefinedAlignment,
                        Image image=null,
                        Image disabledImage=null,
                        color color=nocolor,
                        bool roundedCorners=false,
                        function(Control button) callback=null,
                        SrcRef src=#:src) {

New: public constructor(Window parent,
                        Brush stdBrush,
                        Brush hoverBrush,
                        Brush mouseDownBrush,
                        // inherited key arguments
                        Font font=controlFont,
                        FrameStyle frameStyle=noFrame,
                        frame3DState frameState=frameStateUp,
                        pointI pos=(0, 0),
                        sizeI size=sizeI(-1, -1),
                        pointI margins=(6, 4),
                        alignment align=middle,
                        str key=null,
                        str label="",
                        color labelColor=color(0, 0, 0),
                        color pressedLabelColor=color(colorType.none),
                        alignment textSide=undefinedAlignment,
                        Image image=null,
                        Image disabledImage=null,
                        color color=nocolor,
                        bool roundedCorners=false,
                        bool showUnderline=false, // Added
                        function(Control button) callback=null,
                        SrcRef src=#:src) {

TreeView

A new bool facelift=false argument is added to TreeView's constructor.

Old: public constructor(Window parent,
		                // inherited key arguments
		                Font font=systemFont(),
		                Brush brush=null,
		                FrameStyle frameStyle=stdLightFrame,
		                frame3DState frameState=frameStateDown,
		                pointI pos=(0, 0),
		                sizeI size=(100, 100),
		                pointI margins=(-1, -1),
		                alignment align=middle,
		                bool popup=false,
		                bool shadow=false,
		                function (Control control) callback=null,
		                // extended key arguments
		                bool alwaysMultiSelect=false,
		                bool allowMultiSelect=false,
		                bool directSelect=false,
		                bool listBoxStyle=false,
		                treeViewSelectionStyle selectionStyle=treeViewSelectionStyle.undefined,
		                function (Control control) click2Callback=null,
		                bool transparentScrollBars=false,
		                bool noScrollBars=false,
		                str key=null,
		                SrcRef src=#:src) {
New: public constructor(Window parent,
		                // inherited key arguments
		                Font font=systemFont(),
		                Brush brush=null,
		                FrameStyle frameStyle=stdLightFrame,
		                frame3DState frameState=frameStateDown,
		                pointI pos=(0, 0),
		                sizeI size=(-1, -1),
		                pointI margins=(-1, -1),
		                alignment align=middle,
		                bool popup=false,
		                bool shadow=false,
		                function (Control control) callback=null,
		                // extended key arguments
		                bool alwaysMultiSelect=false,
		                bool allowMultiSelect=false,
		                bool directSelect=false,
		                bool listBoxStyle=false,
		                treeViewSelectionStyle selectionStyle=treeViewSelectionStyle.undefined,
		                function (Control control) click2Callback=null,
		                bool transparentScrollBars=false,
		                bool noScrollBars=false,
		                str key=null,
		                bool facelift=false,
		                SrcRef src=#:src) {

SpellChecker

The following methods have a new str languageTag argument, you're required to specify the language to obtain suggestions or replacements from.

Old: extend public str getSuggestions() {
New: extend public str getSuggestions(str languageTag) {

Old: extend public str getReplacement() {
New: extend public str getReplacement(str languageTag) {

PixelDevice

We have consolidated methods drawText and drawTextWithLineHeight by adding int limitLineHeight=-1 to drawText.

Old: final public void drawTextWithLineHeight(str text, rectI bound, rectI clipRect,
                                              int limitLineHeight,
                                              alignment align=middle,
                                              Font font=null,
                                              color textColor=color(0, 0, 0),
                                              color bkColor=color(192, 192, 192),
                                              bool transparency=true,
                                              bool clipping=false) {
Old: final public void drawText(str text, rectI bound, rectI clipRect,
                                alignment align=middle,
                                Font font=null,
                                color textColor=color(0, 0, 0),
                                color bkColor=color(192, 192, 192),
                                bool transparency=true,
                                bool clipping=false) {
New: final public void drawText(str text, rectI bound, rectI clipRect,
                                alignment align=middle,
                                Font font=null,
                                color textColor=color(0, 0, 0),
                                color bkColor=color(192, 192, 192),
                                bool transparency=true,
                                bool clipping=false,
                                int limitLineHeight=-1) { // Added

DibImage resizing function

The function dibResize() (introduced in 16.0 Minor) used to alter the passed in image and return a newly created copy of DibImage. This has now been improved to resize the DibImage in place, and no longer return a new copy of DibImage:

Old: public DibImage dibResize(DibImage im, sizeI newSize, str filter=null) {
New: public void dibResize(DibImage im, sizeI newSize, str filter=null) {

MemoryImage

To retrieve RGB pixel values from a MemoryImage, the following method has been introduced:

Old: final public RawImageData rawImageDataFixed(int targetBPP=-1) {
New: final public byte[] getPixelDataRGB() {

IconFinder

There have been some cleanups, argument ourOnlyHope has been replaced with debug.

Old: final public Image get(str name, bool debug=false, bool ourOnlyHope=true) {
New: final public Image get(str name, bool debug=false) {

Old: final public Image getDisabled(str name, bool ourOnlyHope=true) {
New: final public Image getDisabled(str name, bool debug=false) {

SvgImage

We have removed the MemoryPixelDevice bitmap field in SvgImage to reduce GDI bitmap object counts. It uses a Dib instead, similar to DibImage.

Old: public MemoryPixelDevice bitmap : copy=null, stream=null;
New: public Dib dib : copy=null, stream=null;

ImageSplashProgressControl

The constructors have been consolidated for this class.

Old: public constructor(str key, Image[] images, rectI bound, int durationMs=10000) {
New: public constructor(str key, Image[] images, rectI bound, int durationMs=10000, bool cycle=false, bool endWithUber=false) {

CircularProgressPainter

The access modifiers for field centerImage has been changed, a new setter method is introduced.

Old: public Image centerImage;
New: private Image centerImage : copy=reference, public readable;
New: final public bool setCenterImage(Image i) {

class GridWindow

The following interfaces have been changed or added in GridWindow for v16.5.

Old: extend public void insertRow(int y, str label=null, bool update=true) {}
New: extend public void insertRow(int y, str label=null, bool update=true, bool updateRowIndexLabels=false) {}

Old: extend public void insertRows(int y, int n) {}
New: extend public void insertRows(int y, int n, bool updateRowIndexLabels=false) {}

Old: extend public void removeRow(int y, bool update=true) {}
New: extend public void removeRow(int y, bool update=true, bool updateRowIndexLabels=false) {}

New: extend public void updateRowIndexLabels(int startRow=0) {}
New: extend public int appendRow(GridCell[] rowCells, str label=null, bool update=true) {}
New: extend public void insertRow(int y, GridCell[] rowCells, str label=null, bool update=true, bool updateRowIndexLabels=false) {}

Added updateRowIndexLabels Parameter

The bool updateRowIndexLabels parameter (default false) on the interfaces above was added in v16.5. It's purpose is to update row labels when row indexes change.

Important to note that if the rows are labeled as anything other than the row index, this parameters value should be set to false. It will update the row labels from the inserted/removed index to the end of the row sequence, setting the labels to the appropriate row index.

See documentation for the updateRowIndexLabels(int startRow=0) function for more info.

extend public void insertRow(int y, str label=null, bool update=true, bool updateRowIndexLabels=false) {
extend public void insertRows(int y, int n, bool updateRowIndexLabels=false) {
extend public void removeRow(int y, bool update=true, bool updateRowIndexLabels=false) {
	...
	
New:	if (updateRowIndexLabels) updateRowIndexLabels(startRow=y);

	...	
}

Added: extend public void updateRowIndexLabels(int startRow=0) {}

Updates the labels of grid rows so they display their current row index.

  • Iterates from the given startRow (default = 0, the first row).
  • Replaces each row’s label with its 1-based index (row + 1).
  • Negative startRow values are corrected to 0.
/**
 * Update labels of rows.
 * This will replace row labels with their index value.
 * @startRow optional row to begin label updates at; default 0 (top/first row)
 */
extend public void updateRowIndexLabels(int startRow=0) {
	if (startRow < 0) startRow = 0;
	while (int row = startRow; row < rowCount; row++) {
		setRowLabel(row, NameGridLabel((row+1).toS()));
	}
}

Added: extend public int appendRow(GridCell[] rowCells, str label=null, bool update=true) {}

Appends a new row of GridCells to the grid and returns its index.

Parameters
  • rowCells → sequence of GridCells to populate the new row.
  • label → optional label for the row.
  • update → whether to auto-update sizing and UI after insertion.
Return

The row index of the newly added row.

Added: extend public void insertRow(int y, GridCell[] rowCells, str label=null, bool update=true, bool updateRowIndexLabels=false) {}

Appends a new row of GridCells to the grid at a given index.

Parameters
  • y → index of row in grid to be inserted
  • rowCells → sequence of GridCells to populate the new row.
  • label → optional label for the row.
  • update → whether to auto-update sizing and UI after insertion.
  • updateRowIndexLabels → optional flag to update row index labels after insertion

cm.win.advanced

class ExploreDialog

Changed getThumbnail() to return a DibImage instead of a MemoryImage to reduce GDI Object usage.

Old: extend public MemoryImage getThumbnail(Url file) {
New: extend public DibImage getThumbnail(Url file) {

custom.accessories.symbols

class SymbolPicklistPart

Constructor Migration: List Price → Base Price + Option Price Sum

In v16.5, a new constructor has been introduced for SymbolPicklistPart to support the new pricing model. Previously, SymbolPicklistPart accepted a cached list price (listPrice) directly as a parameter. This behavior is now being phased out.

Old Constructor
  • constructor(Snapper snapper, ..., double listPrice, ...)
  • Accepts a total listPrice value to cache.
  • See documentation in compile-time section for cm.core.part.Part for more info
New Constructor (preferred, introduced in 16.5)
  • constructor(Snapper snapper, ..., double basePrice, Double optionPriceSum, ...)
  • Accepts basePrice and optionPriceSum separately for caching.
  • See documentation in compile-time section for cm.core.part.Part for more info
Impact
  • Migrate to new constructor and split your values into basePrice and optionPriceSum.
  • All new core features and pricing rules will rely on the new constructor/pricing system.
  • The old constructor remains for backward compatibility

custom.guarding

class GrdLengthPart

Constructor Migration: List Price → Base Price + Option Price Sum

In v16.5, a new constructor has been introduced for GrdLengthPart to support the new pricing model. Previously, GrdLengthPart accepted a cached list price (listPrice) directly as a parameter. This behavior is now being phased out.

Old Constructor
  • constructor(Snapper snapper, ..., double listPrice=0, ...)
  • Accepts a total listPrice value to cache.
  • See documentation in compile-time section for cm.core.part.Part for more info
New Constructor (preferred, introduced in 16.5)
  • constructor(Snapper snapper, ..., double basePrice=0, Double optionPriceSum=0, ...)
  • Accepts basePrice and optionPriceSum separately for caching.
  • See documentation in compile-time section for cm.core.part.Part for more info
Impact
  • Migrate to new constructor and split your values into basePrice and optionPriceSum.
  • All new core features and pricing rules will rely on the new constructor/pricing system.
  • The old constructor remains for backward compatibility

Runtime/Behavior Changes

Compiler and CM Language

Context Dump

A context dump has been added to the current crash information. This dump will do a less intrusive stack dump. The first frame will contain a dump of all registers, a +-32 byte dump around IP, and a +-512 byte dump around SP. The consequtive frames will dump the nonvolatile registers.

The regular stack dump can be found below the context dump.

cet.runtime

mcCheckForUpdatesHandler.cm

  • Changed the design of the notification banner when background install fails.
  • When the user closes CET, CET will now prompt the user to cancel any currently running updates before proceeding to exit CET.
  • Previously, the background install process attempted to register catalogues during the customizations Extension initialization step, which occurs as a side effect when the installer performs a sanity check by loading the install extensions. In some cases, this could result in the main CET instance encountering locked catalogue databases if they were in use by the background instance (and vice versa).
    Since catalogue downloads are managed by the foreground instance, the background instance is now no longer connected to the downloader. Manufacturers should note that any code using while loops to delay extension initialization until catalogue downloads complete may need adjustment, as this is not recommended behavior (or at least to avoid this during runningInInstallAndCloseMode).

cm.abstract.dataSymInterface.catalog

Changed: extend public double price(DsiPData data)

The logic for retrieving prices in the price method has been simplified by removing an unnecessary ProductCatalog reference check.

extend public double price(DsiPData data) {
	...
	
	if (data and prices and prices.seq.any) {
Old:		if (ProductCatalog prdCat = data.prdCatalog()) {
			if (PricelistType pricelist = data.pricelist()) {
				...
			}
		}
		return -0.0001;
	}
	return 0.0;
}

Old → New behavior

Old:

  • Previously, the method attempted to retrieve a ProductCatalog (data.prdCatalog()) before resolving the PricelistType
  • If no ProductCatalog was found, the logic would not proceed to pricing.

New:

  • The ProductCatalog check has been removed.
  • The method now only validates that a PricelistType exists.
  • If present, it retrieves the price and applies factors via addPriceFactors.

cm.abstract.dataSymbol

class DsPData

The following functions were introduced or modified to support the transition to the new part pricing system. Find more documentation on the new pricing system in the compile-time section for cm.core.part.Part.

Added: extend public bool useNewPartPricing() {}
Added: extend public Double optionPriceSum(SpecOption[] opts, Part part=null) {}
Added: extend public DsPart createPart(Snapper snapper, PartsEnv env, double basePrice, Double optionPriceSum) {}
Changed: extend public void getParts(Snapper snapper, PartsEnv env) {}

Added: extend public bool useNewPartPricing() {}

Indicates whether the new part pricing system should be used when generating Parts. An opt-in feature flag to the new pricing system.

Behavior
  • If a proxy is present, defers to proxy implementation.
  • Defaults to returning false (need to opt-in to new pricing system)
  • Versioning Notes
    • OLD system (≤16.0): Pricing based on cached list price only.
    • NEW system (≥16.5): Pricing based on cached base price + optionPriceSum.
Impact
  • Acts as the feature flag for determining which pricing path is followed in constructors, getParts, and createPart.
  • Developers should begin migrating logic to use base price + option price sum rather than raw list price.

Added: extend public Double optionPriceSum(SpecOption[] opts, Part part=null) {}

Calculates the total option prices from a collection of SpecOptions. Optionally takes a Part parameter to allow for special pricing scenarios.

Behavior
  • If a proxy is present, delegates calculation to the proxy.
  • Otherwise, iterates through provided options and sums upcharges.
  • Returns the sum wrapped in a Double.
Impact
  • Centralizes logic for calculating option-based pricing.
  • Developers no longer need to manually aggregate upcharges for SpecOptions.
  • Provides proxy support for flexible overrides.

Added: extend public DsPart createPart(Snapper snapper, PartsEnv env, double basePrice, Double optionPriceSum) {}

The createPart method was split into two overloads to support the new pricing framework. The new overload takes basePrice and optionPriceSum, while the old single-parameter version (using list price) has been marked for deprecation.

Old → New behavior

Old:

  • Single method signature:
    • createPart(Snapper snapper, PartsEnv env, double price)
  • Relied on a unified “list price” model.
  • Retained for backward compatibility.
  • Still used when useNewPartPricing is false

New:

  • New primary method:
    • createPart(Snapper snapper, PartsEnv env, double basePrice, Double optionPriceSum)
  • Constructs parts with explicit basePrice and optional optionPriceSum
  • Used when useNewPartPricing is true
Impact
  • Developers should migrate to the new overload using basePrice and optionPriceSum
  • Better alignment with the new pricing system, which separates base price and option prices.

Changed: extend public void getParts(Snapper snapper, PartsEnv env) {}

The getParts method was updated to integrate the new pricing model.

Old → New behavior

Old:

  • Always calculated part price using price(options)
  • Always created parts with createPart(..., price)
  • Cached copies of parts but did not reset option price sum state.
Old:
extend public void getParts(Snapper snapper, PartsEnv env) {
	...

	if (usePartsCache and cachedParts.any) {
		...
	} else if (prd) {
		...	    
	    double price = price(options);
		...
	    DsPart dpart = createPart(snapper, env, price);
	   ...
	}
}

New:

  • Introduces flag:
    • bool newPricing = useNewPartPricing();
    • If true → price comes from basePrice(), and new createPart uses new signature
    • If false → price comes from price(options), and createPart uses legacy signature
  • Cached part copies now call invalidateOptionPriceSum() to reset option-based pricing.
New:
extend public void getParts(Snapper snapper, PartsEnv env) {
	...

	if (usePartsCache and cachedParts.any) {
		for (c in cachedParts) {
			Part cpy = c.copy;
			...
			cpy.invalidateOptionPriceSum();
		}
	} else if (prd) {
		...
		bool newPricing = useNewPartPricing();
		double price = newPricing ? basePrice() : price(options);
		...
		DsPart dpart = newPricing ? createPart(snapper, env, price, null) : createPart(snapper, env, price);
		
		...
	}
}
Impact
  • Pricing flexibility: supports both old list price calculation and new base price + option prices model
  • Cache correctness: avoids stale option price values by invalidating option price on cached Parts
  • Compatibility: existing logic still works when useNewPartPricing() is disabled.

SIF Export

With the addition of the new part attribute description/notes system, DsPData now exports these values during SIF exports in generateConfiguraSifRows(..):

final package str[] generateConfiguraSifRows(DsPart part, Space space) {
	...

		//Attributes
New:	part.generateSifAnnotationRows(env.lines);

	return res;
}

class DsFreeformPData

Changed: extend public void initPricelist() {}

The logic for initializing a pricelistRef was simplified by replacing an early return with direct initialization when the reference is empty.

Old → New behavior

Old:

  • If pricelistRef was already set, the method returned immediately and did nothing.
  • Otherwise, it explicitly set pricelistRef to the current currency symbol before creating a new PricelistType.
Old: 
extend public void initPricelist() {
	if (pricelistRef.any()) return;
	str code = currentCurrencySymbol();
	pricelistRef = code;
	Date currentDate(date());
	dataCatalog.pricelists.put(pricelistRef, PricelistType(pricelistRef, pricelistRef, currentDate));
}

New:

  • If pricelistRef is empty, it is set to the current currency symbol.
  • Regardless of prior state, the method always inserts/updates the pricelistRef entry in dataCatalog.pricelists
New:
extend public void initPricelist() {
	if (pricelistRef.empty()) pricelistRef = currentCurrencySymbol();

	Date currentDate(date());
	dataCatalog.pricelists.put(pricelistRef, PricelistType(pricelistRef, pricelistRef, currentDate));
}

Changed: public DsPart createPart(..) {}

The createPart API was extended to support a new pricing model with separate basePrice and optionPriceSum parameters. The original listPrice version remains for backward compatibility but is now marked for deprecation.

Old → New behavior

Old:

  • Single createPart method signature:
    • public DsPart createPart(Snapper snapper, PartsEnv env, double price)
  • Pricing was passed as a single price (list price)
  • Called in getParts when useNewPartPricing is false

New:

  • New primary createPart method signature:
    • public DsPart createPart(Snapper snapper, PartsEnv env, double basePrice, Double optionPriceSum)
    • Separates base price from option price additions, allowing more flexible and accurate pricing.
  • Called in getParts when useNewPartPricing is true
Impact
  • More transparent pricing calculation with clearer separation of base vs. option-based prices
  • Developers should migrate from using listPrice to passing basePrice and optionPriceSum

Changed: public void getParts(Snapper snapper, PartsEnv env) {}

The getParts method was updated to support the new pricing model which is documented in the compile-time section for cm.core.part.Part. Instead of always creating parts with a single price, it now conditionally uses basePrice when the new pricing system is enabled.

Old → New behavior

Old:

  • Always created parts using:
    • DsPart dpart = createPart(snapper, env, freeformItem.price());
  • Relied solely on list price (price) for part creation.
Old:
public void getParts(Snapper snapper, PartsEnv env) {
	...
	if (usePartsCache and cachedParts.any) {
		...
	} else {
		DsPart dpart = createPart(snapper, env, freeformItem.price());
	    ...
	}
}

New:

  • Introduces conditional logic via useNewPartPricing():
    • If enabled → parts are created using basePrice (and optionPriceSum = null).
    • If disabled → falls back to old price-based method.
  • Caching and spec option handling remain unchanged.
New:
public void getParts(Snapper snapper, PartsEnv env) {
	...
	if (usePartsCache and cachedParts.any) {
		...
	} else {
		DsPart dpart;
		if (useNewPartPricing()) {
			dpart = createPart(snapper, env, freeformItem.basePrice(), null);
		} else {
			dpart = createPart(snapper, env, freeformItem.price());
		}
		...
	}
}
Impact
  • Supports transition to the new pricing system without breaking existing functionality.
  • Developers must be aware that part creation may now depend on either basePrice or price, depending on configuration.

class DsFreeformPicklistPart

Constructor Behavior Change: List Price → Base Price + Option Price Sum

The constructor for parts created from DsFreeformPData was updated to support the new pricing model. It now conditionally passes basePrice and optionPriceSum to the superclass instead of always relying on listPrice.

Old → New behavior

Old:

  • Always invoked superclass with DsFreeformItem price:
    • super(snapper, fpData, fpData.freeformItem.price(), qty);
  • Relied solely on list price for part initialization.

New:

  • Constructor now checks useNewPartPricing():
    • If enabled → calls superclass with basePrice and optionPriceSum=null.
    • If disabled → calls superclass with listPrice as before.
Impact
  • Parts are initialized with either listPrice or basePrice depending on pricing model configuration.
  • Developers relying on constructor parameters should be aware of the new basePrice/optionPriceSum path.

class DsPDataProxy

The following functions were introduced or modified to support the transition to the new part pricing system. Find more documentation on the new pricing system in the compile-time section for cm.core.part.Part.

Added: extend public bool useNewPartPricing(DsPDataProxyEnv dsEnv) {}
Added: extend public Double optionPriceSum(DsPDataProxyEnv dsEnv, SpecOption[] opts, Part part=null) {}
Added: extend public DsPart createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double basePrice, Double optionPriceSum) {}

Added: extend public bool useNewPartPricing() {}

Allows proxy-aware code to check whether the new pricing system is enabled.

Behavior
  • Returns false by default. Need to opt-in to using new pricing system.
Impact
  • Enables consistent feature flag checks for new vs. old pricing when executing through proxies

Added: extend public Double optionPriceSum(DsPDataProxyEnv dsEnv, SpecOption[] opts, Part part=null) {}

A new overload of optionPriceSum was introduced to support execution through a DsPDataProxy, enabling proxy-aware option pricing calculations.

Behavior
  • Temporarily increments blockDataProxy to prevent recursive proxy calls.
  • Delegates the actual calculation to dsEnv.data.optionPriceSum(opts, part).
  • Ensures blockDataProxy is decremented in a finally block for safety.
Impact
  • Required when option price calculation must occur through a proxy environment (DsPDataProxyEnv).
  • Complements the existing non-proxy optionPriceSum(SpecOption[], Part) method.
  • Provides consistency in environments where proxy delegation is active.
  • Developers writing proxy-enabled logic should use this overload when a DsPDataProxy is available to ensure correct delegation.

Added: extend public DsPart createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double basePrice, Double optionPriceSum) {}

The createPart method was split into two overloads to support the new pricing framework. The new overload takes basePrice and optionPriceSum, while the old single-parameter version (using list price) has been marked for deprecation.

Old → New behavior

Old:

  • Single method signature:
    • createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double price)
  • Relied on a unified “list price” model.
  • Retained for backward compatibility.
  • Still used when useNewPartPricing is false

New:

  • New primary method:
    • createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double basePrice, Double optionPriceSum)
  • Returns null by default
  • Used when useNewPartPricing is true
Impact
  • Proxy providers may implement this method to customize createPart behavior in the new pricing system.
  • Better alignment with the new pricing system, which separates base price and option prices.

cm.abstract.dataSymbol.ui.explorer

Changed: public void readProduct(str code, Scanner s)

The readProduct method was refactored to read the I1 SIF value during product import.

Old → New behavior

Old:

  • Did not support importing of I1 SIF code
    • No support of importing base price for DsFreeformItems

New:

  • Imports I1 to DsFreeformItem base price property

Changed: public void readOptions(str code, Scanner s, DsFreeformItem product)

The readOptions method was refactored to read the O1 SIF value during product option import.

Old → New behavior

Old:

  • Did not support importing of O1 SIF code
    • No support of importing option prices mapped to O1 for DsFreeformItems

New:

  • Imports O1 to DsFreeformItem price property for options/features

cm.abstract.engine.ofml

The following functions were introduced in OfmlPData to support the transition to the new part pricing system. Find more documentation on the new pricing system in the compile-time section for cm.core.part.Part and runtime section for cm.abstract.dataSymbol.DsPData.

Added: public double basePrice() {}
Added: public void specOption(DsSpecOption[] sOptions, Space space, SFeature f,
								Option o, DsPDataOption pDataOption, int level,
								bool choosableInCalculation) {}
Added: extend public void setOptionPrice(SpecOption option) {}
Added: public DsPart createPart(Snapper snapper, PartsEnv env, double basePrice, Double optionPriceSum) {}

Added: public double basePrice() {}

An override of basePrice has been added to OFMLPData to support the migration path to the new pricing model (≥16.5).

Old → New behavior

Old:

  • Delegated to superclass unless overridden.

New:

  • If useNewPartPricing() → returns only B-level row value from the pricelist.
  • If not → falls back to legacy super(..) implementation.

Impact

  • Returns base price in row (level "B") in the pricelist table.
  • Isolated logic (was previously inside price() method).

Added: public void specOption(DsSpecOption[] sOptions, Space space, SFeature f, Option o, DsPDataOption pDataOption, int level, bool choosableInCalculation) {}

The specOption(..) function has been overridden in OFMLPData to support pricing on an OFMLParts SpecOptions.

Old → New behavior

Old:

  • Delegated to superclass unless overridden.
  • specOption() never adjusted option pricing directly

New:

  • If new pricing is enabled → last option in the list gets priced via setOptionPrice()
  • If old pricing → bypasses new behavior (keeps legacy flow).

Impact

  • Ensures new options get priced correctly when added
  • Supports migration to the new pricing model (≥16.5).

Added: extend public void setOptionPrice(SpecOption option) {}

Added as a helper to the overridden specOption(..) function for setting SpecOption price.

Behavior

  • Looks up option rows (level "X") in the pricelist table.
  • Sets option.price directly.

Added: public DsPart createPart(Snapper snapper, PartsEnv env, double basePrice, Double optionPriceSum) {}

The createPart method was split into two overloads to support the new pricing framework. The new overload takes basePrice and optionPriceSum, while the old single-parameter version (using list price) has been marked for deprecation.

Old:

  • Single method signature:
    • createPart(Snapper snapper, PartsEnv env, double price)
  • Relied on a unified “list price” model.
  • Retained for backward compatibility.
    • Still used when useNewPartPricing is false

New:

  • New primary method:
    • createPart(Snapper snapper, PartsEnv env, double basePrice, Double optionPriceSum)
  • Constructs parts with explicit basePrice and optional optionPriceSum
  • Used when useNewPartPricing is true

Impact

  • Developers should migrate to the new overload using basePrice and optionPriceSum
  • Better alignment with the new pricing system, which separates base price and option prices.

Added: public DsPart createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double basePrice, Double optionPriceSum) {}

The createPart method was split into two overloads to support the new pricing framework. The new overload takes basePrice and optionPriceSum, while the old single-parameter version (using list price) has been marked for deprecation.

Old → New behavior

Old:

  • Single method signature:
    • createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double price)
  • Relied on a unified “list price” model.
  • Retained for backward compatibility.
  • Still used when useNewPartPricing is false

New:

  • New primary method:
    • createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double basePrice, Double optionPriceSum)
  • Constructs Parts under the new pricing model constructor
  • Used when useNewPartPricing is true

Impact

  • Proxy providers may implement this method to customize createPart behavior in the new pricing system.
  • Better alignment with the new pricing system, which separates base price and option prices.

cm.abstract.k2.data

Changed: final private DsPart getPartsInternal(DsPDataProxyEnv dsEnv, PartsEnv env) {}

The internal getPartsInternal method now conditionally supports the new part pricing system. It can construct parts using either the legacy list-price–based approach or the new base-price–plus-options approach depending on useNewPartPricing().

Old → New behavior

Old:

  • Always retrieved a list price via:
    • double price = dsEnv.data.price(options);
  • SpecOption[] options were always passed into price() to calculate the full part cost.
  • Single pricing flow, no differentiation between base vs option pricing.

New:

  • Determines pricing strategy based on dsEnv.data.useNewPartPricing():
    • If true (new system):
      • Uses basePrice directly.
      • Passes null for optionPriceSum when calling createPart().
    • If false (legacy system):
      • Still calculates price from price(options) as before.

Impact

  • Introduces dual pricing support inside getPartsInternal()
  • Ensures compatibility during migration:
    • Legacy parts behave the same with no changes required.
    • New pricing model (≥16.5) leverages basePrice + option pricing separation.
  • Developers migrating to the new model should update workflows to split final list price into base price and option price sum

Added: public DsPart createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double basePrice, Double optionPriceSum) {}

The createPart method was split into two overloads to support the new pricing framework. The new overload takes basePrice and optionPriceSum, while the old single-parameter version (using list price) has been marked for deprecation.

Old → New behavior

Old:

  • Single method signature:
    • createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double price)
  • Relied on a unified “list price” model.
  • Retained for backward compatibility.
  • Still used when useNewPartPricing is false

New:

  • New primary method:
    • createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double basePrice, Double optionPriceSum)
  • Constructs Parts under the new pricing model constructor
  • Used when useNewPartPricing is true

Impact

  • Proxy providers may implement this method to customize createPart behavior in the new pricing system.
  • Better alignment with the new pricing system, which separates base price and option prices.

cm.abstract.kitchen

Constructor Migration: List Price → Base Price + Option Price Sum

The multiple Kitchen Part class constructors have been expanded and split to support the new part pricing system (introduced in 16.5). New constructors now accept basePrice and optionPriceSum separately.

Old constructors that use list price will remain available until old pricing system is deprecated.

Classes affected:

  • KitchenFundamentalPart
  • KitchenSimpleLengthPart
  • KitchenDividedLengthPart
  • KitchenRackPart
  • KitchenBaseGablePart
  • KitchenFixedLengthPart
  • KxWorldPart

Impact

  • If you use list price constructors today → update to the new basePrice + optionPriceSum pattern
  • See new pricing API documentation in cm.core.part.Part compile-time section for more migration tips

New fields are available for some classes.

KitchenAutoMeasureGroupSnapper

New : public str key;

KitchenAutoMeasureGroup

New: public guid gid;
New: public str{} snapperGidSet();
New: public KitchenAutoMeasure->Snapper measureSnapperMap() : copy=null;
New: public symbol spaceVolumeId;

New methods are available for some classes.

KitchenAutoMeasureGroup

New: final public void generateGid(Int idx=null) {

KitchenGmc

New: extend public bool allowAutoElevations() : null=false {

cm.abstract.kitchen.externals

Changed: final private DsPart getPartsInternal(DsPDataProxyEnv dsEnv, PartsEnv env) {}

The internal getPartsInternal method now conditionally supports the new part pricing system. It can construct parts using either the legacy list-price–based approach or the new base-price–plus-options approach depending on useNewPartPricing().

Old → New behavior

Old:

  • Always retrieved a list price via:
    • double price = dsEnv.data.price(options);
  • SpecOption[] options were always passed into price() to calculate the full part cost.
  • Single pricing flow, no differentiation between base vs option pricing.

New:

  • Determines pricing strategy based on dsEnv.data.useNewPartPricing():
    • If true (new system):
      • Uses basePrice directly.
      • Passes null for optionPriceSum when calling createPart().
    • If false (legacy system):
      • Still calculates price from price(options) as before.

Impact

  • Introduces dual pricing support inside getPartsInternal()
  • Ensures compatibility during migration:
    • Legacy parts behave the same with no changes required.
    • New pricing model (≥16.5) leverages basePrice + option pricing separation.
  • Developers migrating to the new model should update workflows to split final list price into base price and option price sum

The createPart method was split into two overloads to support the new pricing framework. The new overload takes basePrice and optionPriceSum, while the old single-parameter version (using list price) has been marked for deprecation.

Old → New behavior

Old:

  • Single method signature:
    • createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double price)
  • Relied on a unified “list price” model.
  • Retained for backward compatibility.
  • Still used when useNewPartPricing is false

New:

  • New primary method:
    • createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double basePrice, Double optionPriceSum)
  • Constructs Parts under the new pricing model constructor
  • Used when useNewPartPricing is true

Impact

  • Proxy providers may implement this method to customize createPart behavior in the new pricing system.
  • Better alignment with the new pricing system, which separates base price and option prices.

cm.abstract.material

class COMPart

Constructor Behavior Change: List Price → Base Price + Option Price Sum

The COMPart constructor was updated to explicitly separate base price and option price sum when initializing the part.

Old → New behavior

Old:

  • Constructor directly passed a single list price (pricePerUnit()) into the superclass constructor.
  • No distinction between base price and option-derived price.

New:

  • Constructor passes basePrice=pricePerUnit() and optionPriceSum=0 to the superclass.
  • Establishes a clear separation of pricing components from the start.
  • Aligns COMPart creation with the new part pricing system
Impact
  • COMPart now integrates with the new pricing model that distinguishes base vs option costs.
  • Migration-friendly: existing behavior (price being equal to pricePerUnit()) is preserved since option sum is initialized as 0.
  • Enables future support for option-based price adjustments without changing constructor logic.

cm.abstract.materialHandling

MhFrameSpreadPatternBehavior changes

MhFrameSpreadPatternBehavior now returns false for strictClassification(MhSnapper snapper, str event), which means by default it no longer requires an exact classification LayerSet match for the spread to take place.

MhRowPopulateFunction changes

The existing engine function MhRowPopulateFunction has been replaced with MhRowPopulateFunction2 in the MH abstracts engine function library registered to the key rowPopulate. If you still want to use the old MhRowPopulateFunction, you should override the rowPopulate key in your extension's engine function library to do so.

MhBaySpawner and MhFrameSpawner changes

MhBaySpawner and MhFrameSpawner both now have implementations for configKey() and init(Snapper snapper). These methods were often implemented with the same code in many different extensions so we have now moved the code into these abstract classes. Check your spawner classes to see if these method implementations can be removed from them.

public class MhBaySpawner extends MhStorageSpawner {

    /**
     * Config key.
     */
    public symbol configKey() {
        str s = spnn(super().str, ".", "bay");
        return s.symbol;
    }


    /**
     * Init.
     */
    public void init(Snapper snapper) {
        super(..);
        if (snapper as MhSnapper) {
            snapper.config = createConfig();
        }
    }
}


public class MhSnapperSpawner extends SnapperSpawner {

    /**
     * Create new config.
     */
    extend public MhConfigRef createConfig() {
        return MhConfigRef(guid(), configKey);
    }
}

getAllSnappersInARow function changes

The function getAllSnappersInARow has been updated so that the default value for argument allowBackToBack is now false. allowBackToBack=true will result in including child snappers that do not intersect with the snapper in terms of y-bound. This change was made as that was the more common case in other extensions.

Old: public Snapper{} getAllSnappersInARow(MhSnapper snapper, SnapperFilter filter, bool allowBackToBack=true, Box ownerBound=null, MhSnapper parent=null) {
New: public Snapper{} getAllSnappersInARow(MhSnapper snapper, SnapperFilter filter, bool allowBackToBack=false, Box ownerBound=null, MhSnapper parent=null) {

For cases where you do want to include child snappers despite their y-bound, you will now have to call the function with allowBackToBack=true. This will include products like deep racking which has multiple frames, especially with a "Row" spread pattern.

We have added several methods to make it easier to pass in allowBackToBack=true for MhStorageRowSpreadPattern. You can either subclass MhStorageRowSpreadPattern and override its bool allowBackToBack(MhSnapper snapper) method, or override the snapper shape method bool allowBackToBackSpreadCandidates(MhSnapper snapper, MhSnapperSpreadPattern pattern) in your shape class.

public class MhStorageRowSpreadPattern extends MhSnapperSpreadPattern {

    New:
    /**
     * Allow back-to-back?
     */
    extend public bool allowBackToBack(MhSnapper snapper) {
        return snapper.?allowBackToBackSpreadCandidates(this);
    }
}


public class MhSnapper extends Snapper {

    New:
    /**
     * Allow back-to-back for spread pattern candidates?
     */
    extend public bool allowBackToBackSpreadCandidates(MhSnapperSpreadPattern pattern) {
        if (shape) return shape.allowBackToBackSpreadCandidates(this, pattern);
        return false;
    }
}


public class MhSnapperShape extends CorePropObj {

    New:
    /**
     * Allow back-to-back for spread pattern candidates?
     */
    extend public bool allowBackToBackSpreadCandidates(MhSnapper snapper, MhSnapperSpreadPattern pattern) {
        return false;
    }
}

Examples:

1. Essential Deep Racking
public class DrFrameShape extends GenRackFrameShape {

    /**
     * Allow back-to-back for spread pattern candidates?
     */
    public bool allowBackToBackSpreadCandidates(MhSnapper snapper, MhSnapperSpreadPattern pattern) {
        if (pattern in MhStorageRowSpreadPattern) return true;
        return super(..);
    }
}


2. Essential Cantilever Racking
public class CrStorageRowSpreadPattern extends MhStorageRowSpreadPattern {

    /**
     * Allow back-to-back?
     */
    public bool allowBackToBack(MhSnapper snapper) {
        if (animation as MhSnapperInsertToolAnimation) {
            ?MhSnapper inserter = animation.inserter;
            if (?CrSpreadSidesPatternBehavior b = inserter.?behavior("spreadPattern")) {
                return b.spreadSide.ieq("double");
            }
        }

        return super(..);
    }
}

Changes to selected snappers

As part of the changes to support multi-bays and multi-frames within a single row, we have made some modifications to MhStorageSelectionBehavior. The method void additionalFilteredSnappers(MhSnapper snapper, SnapperSelection sel, Line mouseLine) would append snappers of the same classification and "column" (same x-position) to the selection. This was mainly used for deep racking which would have multiple frames of the same x-position in a single row and usually we would want all of them to be included when one is selected. But this behavior is not wanted for multi-bays/multi-frames where they should be able to be individually selected even though they share the same x-position. We now feel this behavior should not be the default and have added an additional check bool selectAllOfSameColumn(MhSnapper snapper) to block or allow this behavior.

public class MhRowChildSelectionBehavior extends MhStorageSelectionBehavior {

    /**
     * AdditionalFilteredSnappers
     */
    extend public void additionalFilteredSnappers(MhSnapper snapper, SnapperSelection sel, Line mouseLine) {
        if (!selectAllOfSameColumn(snapper)) return;

        Snapper{} visited();
        SnapperFilter f = additionalFilter(snapper);

        ?MhSnapper row = snapper.rootParent;
        MhSnapper[] group = myRowGroup(row, visited, mhRowNotAisleFilter);

        for (r in group) {
            forChildren(c in r) {
                if (f.accepts(c)) {
                    sel << c;
                    forChildrenRecursive(cChild in c) {
                        sel << cChild;
                    }
                }
            }
        }
    }


    /**
     * AdditionalFilter
     */
    extend public SnapperFilter additionalFilter(MhSnapper snapper) {
        return MhCombinedFilter(MhClassificationSnapperFilter(snapper.classification),
                                MhSameColumnSnapperFilter(snapper));
    }


    New:
    /**
     * Return true if should select all child of the same column in a row.
     */
    extend public bool selectAllOfSameColumn(MhSnapper snapper) {
        if (!snapper.rootParent.?isDoubleDeep) return false;
        return true;
    }
}

We have also added a new behavior MhDeepstorageFrameSelectionBehavior in cm.abstract.materialHandling.storage.racking.deepstorage that overrides bool selectAllOfSameColumn(MhSnapper snapper) so that deep racking frames will still all be selected together. If you have a deep racking product but your frame spawner does not extend from MhDeepstorageFrameSpawner, make sure to append this new behavior to your frame spawner class. You should also do the same for other non-deep racking products that do require this behavior.

public class MhDeepstorageFrameSelectionBehavior extends MhRowChildSelectionBehavior {

    /**
     * Return true if should select all child of the same column in a row.
     */
    public bool selectAllOfSameColumn(MhSnapper snapper) {
        return true;
    }
}

Example usage:

public class MhDeepstorageFrameSpawner extends MhRackFrameSpawner {

    /**
     * CustomOtherBehaviors.
     */
    public MhBehavior[] customOtherBehaviors() {
        MhBehavior[] res = super(..);
        res.exclude(mhRowChildSelectionBehavior);
        res << mhDeepstorageFrameSelectionBehavior;
        return res;
    }
}

Changes to spread tools and dimension propagation

As part of the changes to support multi-bays and multi-frames within a single row, one aspect to consider is how to handle configurations (MhConfigRef). Typically a bay/frame that owns a non-temporary config will be visible in the bay/frame editor dialogs, and bays often display their config names in 2D.

One thing we have added to help support allowing either the child bays or the multi bay to own the non-temporary config is by adding this new method bool hasRealConfig(). Such snappers are considered to have a "real config" and is treated as a "config owner".

public class MhSnapper extends Snapper {

    /**
     * Check whether this snapper has config that is not temp.
     */
    extend public bool hasRealConfig() {
        if (config) return !config.temp;
        return false;
    }
}

We have also added a new SnapperFilter class MhConfigOwnerFilter that uses this new check.

public class MhConfigOwnerFilter extends SnapperFilter {

    /**
     * Accept
     */
    public bool accepts(Snapper s) {
        if (s as MhSnapper) {
            if (!s.hasRealConfig) return false;
        }
        return super(..);
    }
}

This new filter is now used in bay and frame spread tools to improve the experience of picking bays/frames as candidates. Now only "config owners" will be picked by these animations and snappers with temporary configs will be ignored. Note that this means if your snappers simply do not use "real configs" but you want the spread tools to work with them, you will have to override these filter methods to exclude mhConfigOwnerFilter.

public class MhBayPickupAnimation extends MhSnapperPickupAnimation {

    /**
     * Candidate filter.
     */
    public SnapperFilter candidateFilter() {
        static CombinedFilter cb(bayFilter, mhConfigOwnerFilter);
        return cb;
    }
}


public class MhBayApplyAnimation extends MhSnapperApplyAnimation {

    /**
     * CandidateFilter
     */
    public SnapperFilter candidateFilter() {
        SnapperFilter sf = super();
        return CombinedFilter(mhNotDoubleDeepRowFilter, mhConfigOwnerFilter, sf);
    }
}


public class MhFramePickupAnimation extends MhSnapperPickupAnimation {

    /**
     * Candidate filter.
     */
    public SnapperFilter candidateFilter() {
        return CombinedFilter(frameFilter, mhConfigOwnerFilter);
    }
}


public class MhFrameApplyAnimation extends MhSnapperApplyAnimation {

    /**
     * CandidateFilter
     */
    public SnapperFilter candidateFilter() {
        SnapperFilter sf = super();
        return CombinedFilter(frameFilter, sf, mhConfigOwnerFilter);
    }
}

MhBayRowEngineBehavior has also received a change related to "config owners". Now when propagating depth or height changes, we will only propagate to bays/frames that are "config owners". This is to prevent propagating a depth change of a child bay to its parent multi bay which should have a larger depth encompassing multiple child bays. Similar to the spread tools, this should be overridden if your product line does not use "real configs" so that dimension propagation continues to work for them.

public class MhBayRowEngineBehavior extends MhEngineBehavior {

    /**
     * Propagating filter.
     */
    extend public SnapperFilter propagatingFilter(MhSnapper noticer, str key) {
        // Make use of spread filter as well.
        SnapperFilter f = classificationFilter(noticer, key);
        MhAssortmentRefSnapperFilter assortmentFilter(noticer.assortmentRef);
        return MhCombinedFilter(assortmentFilter, MhCombinedOrFilter(rowFilter, f));
    }


    /**
     * Classification filter.
     */
    extend public SnapperFilter classificationFilter(MhSnapper noticer, str key) {
        if (key == "w") return noticer.isBay or noticer.parent.?isBay ? bayFilter : frameFilter;
        return MhCombinedFilter(mhConfigOwnerFilter, mhBayOrFrameFilter);
    }
}

Changes to spread tools switchChildren logic

In MhSnapperApplyAnimation, the method void switchChildren(Snapper oldSnapper, Snapper newSnapper) was typically used to retain the current child snappers of the snapper that is currently being applied to (retain oldSnapper.children). This is used for cases where we do not want to replace the child snappers with what was picked up (newSnapper.children).

Changes have been made to support being able to filter out specific child snappers so that they are always switched (so certain oldSnapper.children are always retained upon apply). A new method MhSnapperApplyAnimation.childrenFilter() has been introduced. Previously void switchChildren(Snapper oldSnapper, Snapper newSnapper) would always run its intended logic when executed and the conditional logic is usually checked before calling switchChildren(). We have now moved that logic down into switchChildren() itself so that every child can be specifically checked. If you have overridden switchChildren(Snapper oldSnapper, Snapper newSnapper) or switchSnapper(MhSnapper parent, Snapper oldSnapper, MhSnapper newSnapper), consider moving down any additional logic into switchSnapper().

As for how the childrenFilter() method works, any child snapper that is accepted by the filter will be affected by switchChildren() so those snappers in the pickedUp snapper will not be applied to the applied snapper, or those snappers in the applied snapper will be retained and not removed after the apply. This is useful for cases like certain child snappers should not affect a bay's configuration so the pickUpAndApply should ignore them.

public class MhSnapperApplyAnimation extends MhSnapperSpreadToolAnimation {

    New:
    /**
     * Alternative candidates filter.
     */
    extend public SnapperFilter childrenFilter() {
        return null;
    }


    /**
     * SwitchSnapper
     */
    extend public void switchSnapper(MhSnapper parent, Snapper oldSnapper, MhSnapper newSnapper) {
        ...
        Old: if (!includeChildren) switchChildren(oldSnapper, newSnapper);
        New: switchChildren(oldSnapper, newSnapper);
    }


    Old:
    /**
     * SwitchChildren
     */
    extend public void switchChildren(Snapper oldSnapper, Snapper newSnapper) {
        Snapper{} children = oldSnapper.children;
        
        //remove children from old
        for (child in children) {
            oldSnapper.removeChild(child);
            child.setParent(null);
        }

        //remove newSnappers children
        forChildren(child in newSnapper) {
            newSnapper.removeChild(child);
            child.setParent(null);
        }
        
        //add the old children to new snapper
        for (child in children) {
            child.setParent(newSnapper);
            newSnapper.addChild(child);
        }
    }


    New:
    /**
     * SwitchChildren
     */
    extend public void switchChildren(Snapper oldSnapper, Snapper newSnapper) {
        Snapper{} children = oldSnapper.children;

        SnapperFilter cFilter = childrenFilter;
        //remove children from old
        for (child in children) {
            if (!includeChildren or (cFilter and !cFilter.accepts(child))) {
                oldSnapper.removeChild(child);
                child.setParent(null);
            }
        }

        //remove newSnappers children
        forChildren(child in newSnapper) {
            if (!includeChildren or (cFilter and !cFilter.accepts(child))) {
                newSnapper.removeChild(child);
                child.setParent(null);
            }
        }
        
        //add the old children to new snapper
        for (child in children) {
            if (!includeChildren or (cFilter and !cFilter.accepts(child))) {
                child.setParent(newSnapper);
                newSnapper.addChild(child);
            }
        }
    }
}

There may still be a need to unconditionally execute switchChildren() which is no longer posssible due to the changes to the existing method. As such, we have temporarily introduced a new method void switchChildren(Snapper oldSnapper, Snapper newSnapper, bool forceSwitch) where if forceSwitch=true, will just execute the switchChildren() logic without conditional checks for each child. One such example is in MhLevelApplyAnimation.adjustLevelClassification(MhSnapper oldSnapper, MhSnapper newSnapper), when we are picking-up or applying a top beam that should not have child snappers, so this prevents applying unit load snappers to the top beam.

public class MhSnapperApplyAnimation extends MhSnapperSpreadToolAnimation {

    New:
    /**
     * Switch children.
     */
    extend public void switchChildren(Snapper oldSnapper, Snapper newSnapper,
                                      bool forceSwitch) {
        if (forceSwitch) {
            Snapper{} children = oldSnapper.children;

            //remove children from old
            for (child in children) {
                oldSnapper.removeChild(child);
                child.setParent(null);
            }

            //remove newSnappers children
            forChildren(child in newSnapper) {
                newSnapper.removeChild(child);
                child.setParent(null);
            }
        
            //add the old children to new snapper
            for (child in children) {
                child.setParent(newSnapper);
                newSnapper.addChild(child);
            }
        } else {
            switchChildren(oldSnapper, newSnapper);
        }
    }
}


public class MhLevelApplyAnimation extends MhSnapperApplyAnimation {

    /**
     * Adjust level classification.
     */
    extend public void adjustLevelClassification(MhSnapper oldSnapper, MhSnapper newSnapper) {
        ...
        if (oldSnapper.isTop) {
            newSnapper.classification += {sTop};
            switchChildren(oldSnapper, newSnapper, forceSwitch=true);
        } else if (newSnapper.isTop) {
            newSnapper.classification -= {sTop};
            switchChildren(oldSnapper, newSnapper, forceSwitch=true);
        }
    }
}

Improved support for connector snapping

MhSnapBehavior now also appends alternatives MhAttachSnapAlternative for each connector in the attach target.

public class MhSnapBehavior extends MhBehavior {

    /**
     * Append snap alternatives.
     */
    extend public void appendSnapAlternatives(Snapper snapper, SnapAlternative[] alternatives, Animation a, AnimationMouseInfo mi, SnapperFilter filter=null) {
        ...
        if (!spread) appendAttachSnapAlternatives(snapper, s, r.v1, alternatives, r.v2);
    }


    /**
     * Append attach snap alternative.
     */
    extend public void appendAttachSnapAlternatives(Snapper main, Snapper closest, point ip,
                                                    SnapAlternative[] alternatives, Double distanceToSnapper=null) {
        for (c in closest.?connectors()) alternatives << MhAttachSnapAlternative(c);
    }
}

For MhSnapperInsertToolAnimation, inserting using MhAttachSnapAlternative will now also try to snap the inserting snapper together with the alternative's snapper.

public class MhSnapperInsertToolAnimation extends MhSnapperSpreadToolAnimation {

    /**
     * StdInsert
     */
    public void stdInsert() {
        ...
                    if (alt as AttachSnapAlternative) {
                        beforeSnap(cMain, alt);
                        snapAllAligned({Snapper: cMain, alt.c.?snapper});
                        afterSnap(cMain, alt);
                    }
        ...
    }


    /**
     * Before snap.
     */
    extend public void beforeSnap(MhSnapper snapper, AttachSnapAlternative alt) { }


    /**
     * After snap.
     */
    extend public void afterSnap(MhSnapper snapper, AttachSnapAlternative alt) { }
}

MH animations now allows idle validation

The following classes now return true for the method bool allowIdleValidation(). This change was made so that graphics validation could still occur during animation for large drawings.

  • MhRowInsertToolAnimationG2
  • MhSnapperToolAnimationG2
  • MhDragAnimation
  • MhPropertyStretchAnimation

cm.abstract.ofdaXml

class OfdaXMLOrderLineProxy

With the addition of the new part attribute description/notes system, OfdaXMLOrderLineProxy now exports these values during OFDA XML exports.

The following function was added to do this. It is called from xmlLineItem(..) during a Parts export. It loops through the parts attributes to generate XML tags for them. The tags replicate those used in the Spec application's OFDA XML export.

/**
 * Generate the Part Attributes.
 */
extend public void xmlGeneratePartAttributes(str prefix, Part part, XmlStreamBuf buf) {
	for (attribute in part.?getAnnotations()) {
		xmlHead(prefix # "Comment", buf) {
			xmlItem(prefix # "Type", "Line Note");

			str value = concat(attribute.note, " : ", attribute.description);
			xmlItem(prefix # "Value", value);
		}
	}
}

cm.abstract.part

class ProdPartGridBuilder

Old → New Constructor

Old:

  • Simply assigned the provided env.
  • If env was null, the instance had no default environment.
Old:
public constructor(PartGridBuilderEnv env=null) {
    this.env = env;
}

New:

  • If no environment is passed, it now creates a default ProdPartGridBuilderEnv with buildTotalsRow = true.
  • Ensures a baseline configuration is always available, even without explicit input.
New:
public constructor(PartGridBuilderEnv env=null) {
    this.env = env ?? ProdPartGridBuilderEnv(buildTotalsRow=true);
}

class CustomSpecOption

See cm.abstract.part section in 16.5 New Features migration guide for new CustomSpecOption class documentation .

class AbsPart

Changed: extend public PartInfoTree createInfoTree(SpecOption option, PartInfoTree parent=null) {}

  • AbsPart now returns a SpecOptionInfoTree by default when generating info trees
extend public PartInfoTree createInfoTree(SpecOption option, PartInfoTree parent=null) {
Old:	return null;
New:	return SpecOptionInfoTree(this, option, parent=parent);
}

Old Pricing API

The following functions are now part of the old pricing system. They are still available for backwards compatibility (except basePrice which has been overloaded in core Part). Migration to the new pricing API should happen as soon as possible. The new pricing API is introduced in v16.5 and documented in the cm.core.part.Part documentation.

public double customListPrice(bool includeChildren=false, str currency=null, Space space=null) {}
extend public double specialListPrice(double listPrice) {}
extend public Double baseOptionPrice() {}
extend public Double specialOptionPrice() {}
extend public double optionSpecialUpcharge() {}
extend public Double upcharge() {}

Old: extend public double basePrice() {}
New: extend public double basePrice(bool includeChildren=false, Space space=null, bool translatePrice=true) {}

class AbsBasePricePartColumn

Changed: public Object value(Part part, Space space) {}

The AbsBasePricePartColumn value now calls the new basePrice(..) function on core Part.

Old → New behavior

Old:

  • Checked if part was an AbsPart
  • Returned part.basePrice() if so
  • Returned null if not

New:

  • Returns new basePrice(..) function value directly from core Part

class AbsUpchargePartColumn

Changed: public Object value(Part part, Space space) {}

The AbsUpchargePartColumn value now calls the new optionPriceSum(..) function on core Part.

Old → New behavior

Old:

  • Checked if part was an AbsPart
  • Returned part.upcharge() if so
  • Returned null if not

New:

  • Returns new optionPriceSum(..) function value directly from core Part

class SpecOptionInfoTreeColumn

Changed: public GridCell gridCell(PartInfoTree item, Space space, symbol view=null) {}

Old → New behavior

Old:

  • If there is no value for the column, returns null
  • Otherwise, returns a NameGridCell with the value

New:

  • If there is no value for the column, returns null
  • If the item is a SpecOptionInfoTree, returns a SpecOptionNameGridCell with the items associated value, part, and option
  • Otherwise, returns a NameGridCell with the value
public GridCell gridCell(PartInfoTree item, Space space, symbol view=null) {
	str value = output(item, space);
	if (value.empty()) return null;
	
New:	if (item as SpecOptionInfoTree) {
New:		return SpecOptionNameGridCell(value, item.part, item.specOption, align=left);
New:	}

	return NameGridCell(value, left);
}

class CustomOptionSpecial

See cm.abstract.part section in 16.5 New Features migration guide for new CustomOptionSpecial class documentation .

class SpecOptionInfoTree

Changed: public PartColumn[] columns() {}

Old → New behavior

Old:

  • If _columns field was set, its value was returned
  • Otherwise, super() was returned (which is null)
Old:
public PartColumn[] columns() {
	return _columns ?? super();
}

New:

  • If _columns field was set, its value was returned
  • Otherwise, returns a default set of SpecOption columns
    • specOptionInfoColumn (option code)
    • specOptionDescColumn (option description)
    • specOptionUpchargeColumn (option price)
New:
public PartColumn[] columns() {
	return _columns ?? [PartColumn: specOptionInfoColumn,
						specOptionDescColumn,
						specOptionUpchargeColumn];
}

class ProdPart

Constructor Migration: List Price → Base Price + Option Price Sum

The ProdPart class constructors have been expanded and split to support the new part pricing system (introduced in 16.5). New constructors now accept basePrice and optionPriceSum separately.

Old constructors that use list price remain available but are marked for deprecation in a future version.

Old Constructor
  • constructor(Snapper snapper, str articleCode, str description, double listPrice, …)
  • This constructor uses list price (a single cached price) instead of separating base + options.
  • Still supported in 16.5 for compatibility.
New Constructors (preferred, introduced in 16.5)
  • constructor(Snapper snapper, str articleCode, str description, double basePrice, Double optionPriceSum, …)
  • This constructor uses separate base price and option prices instead of utilizing a single cached list price
  • Preferred in v16.5+
Impact
  • If you use list price constructors today → update to the new basePrice + optionPriceSum pattern
  • See new pricing API documentation in cm.core.part.Part compile-time section for more migration tips

Added: public Double generateOptionPriceSum() {}

Introduces a new method to compute the total option price, explicitly aggregating all SpecOption upcharges, including specials.

Behavior
  • Before:
    • No direct method existed for calculating the total option price.
    • Special handling was inconsistent and often spread across different code paths.
  • Now:
    • Provides a single method that:
      • Iterates through all specOptions().
      • Calls each option’s upcharge(this) for contextual pricing.
      • Returns the summed total as a Double.
Impact
  • Centralizes option price calculation logic.
  • Ensures specials are accounted for in a standardized way.
  • Reduces duplication across pricing code paths.
  • Improves clarity: developers can call one method to retrieve the full option pricing.
  • If you have custom option price logic, override generateOptionPriceSum() in your extended Part class.
  • Invalidate the cache (invalidateOptionPriceSum()) when option state changes to trigger regeneration.

Added: extend public void invalidateSpecOptions(bool invalidateOptionPriceSum=true) {}

The invalidateSpecOptions method is introduced alongside the new SpecOptions caching mechanism.

  • Before: SpecOptions sequence was always rebuilt when accessed.
  • Now: SpecOptions are cached for performance reasons, and this method allows developers to explicitly invalidate (reset) the cache.
  • It also optionally invalidates the cached option price sum, ensuring related derived data stays in sync.
Behavior
  • Sets _specOptions to null, clearing the cache so the sequence will be recomputed on next access.
  • If invalidateOptionPriceSum is true (default), also clears the cached option price sum.
Impact
  • New responsibility for developers:
    • Since SpecOptions are now cached, any change to underlying data that affects them must be followed by a call to invalidateSpecOptions() to prevent stale values
  • Performance improvement:
    • Accessing SpecOptions is now faster due to caching
    • Rebuild happens only when invalidated (see rebuildSpecOptions documentation)
  • Backward compatibility:
    • Code that relied on automatic rebuild will now see cached values
    • Developers must add explicit invalidation calls in update flows where rebuild is required
  • Migration requirement:
    • Review any code paths where SpecOptions can change (e.g., product configuration updates).
    • Insert calls to invalidateSpecOptions() in those paths to ensure cache consistency.
    • If option price sums are affected by the same changes, let the default true flag handle both invalidations. Otherwise, explicitly pass false.

Changed: extend public void appendSpecOptions(SpecOption[] newOptions) {} and extend public void appendSpecOption(SpecOption newOption) {}

Both appendSpecOptions and appendSpecOption now accept a new boolean parameter invalidateOptionPrice (default: true). This works with the new option price caching mechanism by allowing the system to selectively invalidate the cached option price sum when spec options are modified.

Old: extend public void appendSpecOptions(SpecOption[] newOptions) {}
New: extend public void appendSpecOptions(SpecOption[] newOptions, bool invalidateOptionPrice=true) {}

Old: extend public void appendSpecOption(SpecOption newOption) {}
New: extend public void appendSpecOption(SpecOption newOption, bool invalidateOptionPrice=true) {}
Old → New behavior

Old:

  • Option prices were not cached.
  • Every time option prices were accessed, they were recalculated on the fly based on current spec options.
  • No invalidation step was needed.

New:

  • Option price totals are now cached
  • When spec options are modified, invalidateOptionPriceSum() is triggered to ensure the cache stays correct
  • Developers can pass invalidateOptionPrice=false to skip cache invalidation if they plan to manage it manually or delay recalculation.
Impact
  • Performance improvement: frequent option price sum lookups no longer require full recalculation
  • Cache management: introduces the concept of invalidating/regenerating option price sums as options change.
  • Default safety: with the default true, cache stays consistent automatically — existing usage behaves as expected
  • Advanced optimization: developers can set false during bulk option appends/updates, then explicitly trigger invalidation once, avoiding multiple regenerations.
  • Migration note: developers should be aware that option pricing is no longer computed “live” — results are now cache-backed.

Cache SpecOption Changes

With the introduction of custom options in the query dialog, the following changes were made in ProdPart:

  • specOptions() now inserts custom options
    • Inserted at value of CustomSpecOptions sequence value
    • Inserts to end if sequence is -1 (default for CustomSpecOption)
  • Introduced nonCustomSpecOptions()
    • Provides all SpecOptions except CustomSpecOptions
    • Filters out any CustomSpecOption instances while collecting
  • Introduced customSpecOptions()
    • Provides all CustomSpecOptions only.
    • Uses part specials key + PartSpecialHolder lookup to gather user-defined options.
  • Added caching for SpecOptions
    • New field private SpecOption[] _specOptions : stream=null
    • Purpose: cache collects spec options to avoid repeated recollection and insertion
    • Must be invalidated when _partOptions changes
  • specOptions() now cached
    • Old: dynamically built a fresh list of options every call.
    • New: checks _specOptions, rebuilds only if null, otherwise returns cached.
  • Introduced rebuildSpecOptions()
    • Centralizes SpecOption sequence rebuilding
    • Ensures both built-in options and dynamic custom ones are aggregated in a single place
Impact
  • Performance: caching improves efficiency when parts/options are queried frequently
  • Extensibility: system now supports user-defined freeform options (CustomSpecOptions) in addition to standard ones
Interfaces
Added: private SpecOption[] _specOptions : stream=null;
Added: final public SpecOption[] nonCustomSpecOptions() {}
Added: extend public void insertCustomOptions(SpecOption[] ops) {}
Added: extend public SpecOption[] rebuildSpecOptions() {}
Added: extend public void invalidateSpecOptions() {}
Added: extend public CustomSpecOption[] customSpecOptions() {}
Added: extend public CustomOptionSpecial[] getCustomOptionSpecials(PropObj s=null) {}

Exporting Part Attributes via New Interface

With the addition of the new Attribute Note and Description columns in the Calculations dialog, the following changes have been made to ProdPart:

  • Overrode new partToOwnerKey function
    • Uniquely identifies a part to it's owner
      • Ex. table legs with a table top owner could have part-to-owner keys like "leg0", "leg1", etc.
    • Behavior:
      • returns the partSourceID value appended to the super() value
  • Introduced generateSifAnnotationRows function
    • Happens during Configura SIF exports
    • Behavior:
      • loops through Parts PartAnnotations
      • adds SIF lines for each PartAnnotation
        • "AN" for the PartAnnotations note value
        • "AD" for the PartAnnotations description value
  • Introduced generateAttributeData function
    • Happens during PMX exports
    • Behavior:
      • loops through Parts PartAnnotations
      • generates AttributeData objects for each PartAnnotation
      • adds the AttributeData objects to the ItemDatas attributes sequence
  • Introduced initializeAnnotationsFromItemData
    • Happens during PMX imports
    • Behavior:
      • loops through an ItemDatas attributes sequence
      • generates PartAnnotation objects from the AttributeDatas
      • adds the PartAnnotationobjects to thePart`s annotations
// Part-to-Owner key
New: public str partToOwnerKey() {}

// SIF
New: extend public void generateSifAnnotationRows(str[] lines) {}

// PMX
New: extend public void generateAttributeData(ItemData itemData) {}

//PMX Import
New: extend public void initializeAnnotationsFromItemData(ItemData item, ProjectInformation projectInfo) {}

class SpecOptionUpchargeInfoTreeColumn

A new SpecOptionUpchargeInfoTreeColumn has been added to display price values associated with SpecOptions in a PartInfoTree

  • Provides formatting and display logic for the price of each option.
  • Defaults to being hidden in the UI (initialVisibility = #none).
  • Uses a SpecOptionNameGridCell

cm.abstract.part.query

class OptionMakeSpecialDialog

Added: public PartSpecial generateSpecial() {}

The helper method generateSpecial() has been overridden to streamline the process of creating OptionSpecial objects from dialog input.

Behavior
  • If original special was of the OptionSpecial type, calls and returns generateOptionSpecial value
  • Otherwise, returns the super() value

Added: extend public OptionSpecial generateOptionSpecial(OptionSpecial original) {}

The helper method generateOptionSpecial() has been created to create OptionSpecial objects from dialog input.

Behavior
  • If original special was of the CustomOptionSpecial type, creates and returns a CustomOptionSpecial made from the dialogs input values
  • Otherwise, returns an OptionSpecial given the dialogs input values

class QueryOptionRowData

In 16.5, the ProdPartQueryDialogDataEnv has changed it's backing data structures, replacing the str->Part parts, int->str rowIDs, and str->str{} options maps with a single QueryRowData[] queryRows data sequence. As a result, the subclass QueryOptionRowData has been created to represent option rows in the query dialog.

  • Encapsulates a SpecOption and it's parent Part
  • Handles manipulation of the SpecOptions special on the owning Part
  • Contains a new field to track the options parent row (public QueryRowData parent;).

Interface

/**
 * QueryOptionRowData
 * 
 * This class is responsible for managing 
 * data for a SpecOption row in a QueryDialog.
 */
public class QueryOptionRowData extends QueryRowData {

    /**
     * Parent QueryRowData object.
     */
    public QueryRowData parent : copy=reference;

    /**
     * Constructor.
     * @id ID for this row
     * @data The data object to associate with this row
     */
    public constructor(str id, Object data, QueryRowData parent=null) { ... }

    /**
     * SpecOption option associated with this row.
     * @return The SpecOption object associated with this row.
     */
    extend public SpecOption option() { ... }

    /**
     * Parent Part to this row.
     * @recurse optional flag to recurse tree to parent Part
     * @return The Part object associated with this row's parent.
     */
    extend public Part parentPart(bool recurse=false) { ... }

    /**
     * Parent SpecOption to this row.
     * @return The SpecOption object associated with this row's parent.a
     */
    extend public SpecOption parentOption() { ... }

    /**
     * Get the special associated with this row.
     * Override to get the specific special handling logic.
     * @return The PartSpecial object associated with this row
     */
    public PartSpecial getSpecial() { ... }

    /**
     * Assign a special to this row.
     * Override to get the specific special handling logic.
     * @special The PartSpecial object to associate with this row
     */
    public void putSpecial(PartSpecial special) { ... }
    
    /**
     * Remove the special from this row.
     * Override to get the specific special handling logic.
     */
    public void removeSpecial() { ... }
}

class ProdPartQueryControlWindow

New Add Option Button

With the addition of the new "Add Option" feature in the query dialog, a new override of the QueryControlWindow has been added. It contains an additional button (addOptionButton) for the new adding custom options feature.

public class ProdPartQueryControlWindow extends QueryControlWindow {
    
    /**
     * Add custom option button.
     */
    public QueryButton addOptionButton;
    
       
    /**
     * Initialize controls.
     */
    public void initControls() { ... }
    
    
    /**
     * Align controls.
     */
    public void alignControls() { ... }
}

class QueryProdPartRowData

In 16.5, the ProdPartQueryDialogDataEnv has changed it's backing data structures, replacing the str->Part parts, int->str rowIDs, and str->str{} options maps with a single QueryRowData[] queryRows data sequence. As a result, the subclass QueryProdPartRowData has been created to represent ProdPart rows in the query dialog.

  • Only overridden to custom handle putSpecial:
    • Allows CustomOptionSpecials to be created when a ProdPart row is selected in the query dialog

Interface

/**
 * QueryProdPartRowData
 * 
 * This class is responsible for managing 
 * data for a ProdPart row in a QueryDialog.
 */
public class QueryProdPartRowData extends QueryPartRowData {

    /**
     * Put a special to this row.
     * @special The PartSpecial object to put
     */
    public void putSpecial(PartSpecial special) {
        if (special as CustomOptionSpecial) {
            if (data as ProdPart) {
                data.putOptSpecial(special.option(), special);
            }
        } else {
            super(..);
        }
    }
}

cm.abstract.pmx

class ItemData

As of 16.5, a new interface for lead time and PartAnnotations have been added to core Part. These values are now exported with PMX exports leading to the following additions in ItemData.

In cm/abstract/pmx/itemData.cm

New: public AttributeData[] attributes;

/**
 * Instantiate ItemData (this) fields from Part.
 */
extend public void generateItemDataFromPart(Part part) {
	...
New:	this.leadtime = part.leadTime;

	if (part as ProdPart) {
		...
New: 	part.generateAttributeData(this);
		...
	}

	...
}

cm.abstract.projectInfo

class ProjectInformationDialog

Due to an issue where Project Information fields were saving in cases where the user did not click "OK" (dialog close, field editing, etc), the following changes have been made in ProjectInformationDialog.

Changed: private bool saveInfo;

  • The default value has changed to false for the saveInfo field

Added: extend public void updateDependent(str key, str depKey, Object value, bool update=true) {}

  • New bool update (default: true) parameter
  • Allows control over updating project information through dependent controls
  • Solves issue where dependent controls are updating project information by default even if top level control is not updated

Changed: extend public void textContentChanged(Control c, bool update=true) {}

  • Now calls new updateDependent function, passing in the update parameter
/**
 * Text content changed.
 */
extend public void textContentChanged(Control c, bool update=true) {
	...
	
Old:	for (depKey in dependentKeys(key)) updateDependent(key, depKey, val);
New:	for (depKey in dependentKeys(key)) updateDependent(key, depKey, val, update=update);
}

Changed: extend public void enableSelected(Control control, bool update=true) {}

  • Now calls new updateDependent function, passing in the update parameter
/**
 * Check box changed.
 * To be done in child class.
 */
extend public void enableSelected(Control control, bool update=true) {
	if (!world or !control.visible) return;
	if (control as CheckBox) {
		...
		
Old:	for (depKey in dependentKeys(key)) updateDependent(key, depKey, val);
New:	for (depKey in dependentKeys(key)) updateDependent(key, depKey, val, update);
	} else {
		...
	}
}

Changed: extend public void applyAndClose() {}

  • Now sets saveInfo back to default false value after close

Added: public bool keyEsc() {}

  • Overridden to handle saving on escape-click
  • Sets saveInfo to false before calling super()

Changed: extend public FormattedTextField appendFieldCombo(..) {}

Text fields created in this function (FormattedTextField) now set the enterKeyCallback parameter to contentChangedCB rather than applyCB

Old → New behavior

Old:

  • FormattedTextFields used applyCB as their enter-key callback
  • applyCB called window.apply(), automatically applying changes to the world cached project information object

New

  • FormattedTextFields now use contentChangedCB as their enter-key callback
  • Applies changes visually without applying them to the world cached project information object
Impact
  • The ProjectInformationDialog should now only update the world cached ProjectInformation when the user selects "OK" in the dialog

Changed: extend public DateField appendDateFieldCombo(..) {}

Date fields created in this function (DateField) now set the callback parameter to contentChangedCB rather than applyCB

Old → New behavior

Old:

  • DateFields used applyCB as their callback
  • applyCB called window.apply(), automatically applying changes to the world cached project information object

New

  • DateFields now use contentChangedCB as their callback
  • Applies changes visually without applying them to the world cached project information object
Impact
  • The ProjectInformationDialog should now only update the world cached ProjectInformation when the user selects "OK" in the dialog

Changed: private void contentChangedCB(Control control) {}

  • Now calls window.textContentChanged() with update parameter set to false
Old → New behavior

Old:

  • Called window.textContentChanged with update parameter set to default (true)
  • Dialog controls using the contentChangedCB callback would automatically update the world cached project information

New:

  • Calls window.textContentChanged with update parameter set to false
  • Dialog controls using the contentChangedCB callback do not update world project information automatically
Impact
  • The ProjectInformationDialog should now only update the world cached ProjectInformation when the user selects "OK" in the dialog
/**
 * Text area content changed callback
 */
private void contentChangedCB(Control control) {
    Window window = control.parentFrame;
    if (window as ProjectInformationDialog and control.visible) {
Old:		window.textContentChanged(control);
New:		window.textContentChanged(control, update=false);

    }
}

Changed: private void checkBoxContentChangedCB(Control control) {}

Now calls window.enableSelected() with update parameter set to false

Old → New behavior

Old:

  • Called window.enableSelected with update parameter set to default (true)
  • Dialog controls using the checkBoxContentChangedCB callback would automatically update the world cached project information

New:

  • Calls window.enableSelected with update parameter set to false
  • Dialog controls using the checkBoxContentChangedCB callback do not update world project information automatically
Impact
  • The ProjectInformationDialog should now only update the world cached ProjectInformation when the user selects "OK" in the dialog
/**
 * Check Box content changed callback.
 */
private void checkBoxContentChangedCB(Control c) {
    Window window = c.parentFrame;
    if (window as ProjectInformationDialog and c.validAndVisible) {
Old:		window.enableSelected(c);
New:		window.enableSelected(c, update=false);
    }
}

cm.core

Session removing world

When removing a World using removeWorld(World) which is not the main, main will result in being nulled. It should only be nulled if the World being removed is main. This ensures that calling mainWorld() would not be null (or select another world) when you are calling mainWorld during beforeSelectWorldHook, selectWorldHook, removeWorldHook.

Space changeToSpace for snappers

hamt_snapper_insert() is called earlier in changeToSpace() to match the put method. If you have overriden changeToSpace() in your snapper to check if xsnapper is in the Space's snappers_hamt, it will now successfully find it in snappers_hamt where it could previously fail.

Old:
    extend public void changeToSpace(Snapper xsnapper, bool putInBsp=false) {
        ...
        snappers << xsnapper;
        xsnapper.changeToSpace(this);  // changeToSpace gets called before insert to hamt
        xsnapper.space = this;
        holders << xsnapper.holder;

        catchAndReportErrors("Space HAMT insert due to changeToSpace") {
            hamt_snapper_insert(xsnapper, "changed to space", snappers);
        }
        ...
    }

New:
    extend public void changeToSpace(Snapper xsnapper, bool putInBsp=false) {
        ...
        catchAndReportErrors("Space HAMT insert due to changeToSpace") {
            hamt_snapper_insert(xsnapper, "changed to space", snappers);
        }

        snappers << xsnapper;
        xsnapper.changeToSpace(this); // changeToSpace gets called after insert to hamt
        xsnapper.space = this;
        holders << xsnapper.holder;
        ...
    }

Space hamt_snapper_insert

In previous versions, hamt_snapper_insert() method previously evicts an existing Snapper if an existing snapper with the same guid key exists, then prints out a collision report. In 16.5, if a different Snapper is inserted into the hamt with a non-unique guid, a new guid is assigned to the newly inserted Snapper first, prints out a collision report. Followed by inserting the modified snapper into Space.snappers_hamt.

class Animation

Added: extend public WindowView currentView() {

Sometimes tool animations (ToolAnimationG2) would get the incorrect view. This has been resolved by replacing uses of activeView with currentView.

Copy

The original snappers being copied (e.g. by ctrl+c) no longer receive calls to pickedUp(), dropped() and snapAllAligned(). The reason for this change is to avoid issues where the original snappers were unexpectedly modified.

InsertAnimationG2 and DragAnimationG2

InsertAnimationG2 and DragAnimationG2 has been adjusted to avoid trySnap() from being inadvertently called twice within the same action to improve performance.

Session

Starting from 16.5 Major, we are changing the default argument selectWorldIfNull from true to false. This is to prevent cases where calling mainSpace() or mainWorld() can inadvertently mess up the currently selected world, especially when called by various world or space hooks. We encourage you to review existing usage of mainWorld() and mainSpace() to clarify the desired behavior.

Old: final public World mainWorld(bool selectWorldIfNull=true) {
New: final public World mainWorld(bool selectWorldIfNull=false) {

Old: final public Space mainSpace(bool selectWorldIfNull=true) {
New: final public Space mainSpace(bool selectWorldIfNull=false) {

class InputCoreProperty

We have made changes in method void appendControls(Control[] list) when generating a CoreDistanceField so that the measureEndCallback function for this control works with property owners that are not Snapper, Animation, or Vessel.

This sub-function now distinguishes between the owner of the property (which can be any PreCorePropObj), and the owner of the CoreProperties object. Now it can successfully put a new property value into the property owner without needing to cast it to Snapper, Animation, or Vessel. Note that in this case, the owner of the CoreProperties still needs to be a Snapper.

One additional difference is that if the property owner is a Snapper, the callback Snapper.quickPropertyChanged(str key, Object value, Object oldValue) will still be called as it previously was. However if the property owner is not a Snapper, the callback CorePropObj.userPropertyChanged(str key, Object current, Object oldValue, CoreProperties properties) will be called instead.

public class InputCoreProperty extends CoreProperty {

    /**
     * Append controls.
     */
    public void appendControls(Control[] list) {
        ...
        <CorePropObj, PreCorePropObj, str> prop;
        if (?prop = env) {
            ...
            Object propertiesOwner = prop.v0;
            if (?Snapper z = propertiesOwner) {
                ...
                prop.v1.put(prop.v2, nw);

                if (prop.v1 == z) {
                    z.quickPropertyChanged(prop.v2, nw, old);
                } else if (prop.v1 in CorePropObj) {
                    prop.v1.CorePropObj.userPropertyChanged(prop.v2, nw, old, null);
                }
                ...
    }
}

Constructor Migration: List Price → Base Price + Option Price Sum

In v16.5, new constructors have been introduced for the above Part classes to support the new pricing model. Previously, these Parts accepted a cached list price (listPrice) directly as a parameter. This behavior is now being phased out.

Old Constructors

  • constructor(Snapper snapper, str articleCode, str description, double listPrice, ...)
  • Accepts a total listPrice value to cache.
  • See documentation in compile-time section for cm.core.part.Part for more info

New Constructor (preferred, introduced in 16.5)

  • constructor(Snapper snapper, str articleCode, str description, double basePrice, Double optionPriceSum, ...)
  • Accepts basePrice and optionPriceSum separately for caching.
  • See documentation in compile-time section for cm.core.part.Part for more info

Impact

  • Migrate to new constructor and split your values into basePrice and optionPriceSum.
  • All new core features and pricing rules will rely on the new constructor/pricing system.
  • The old constructor remains for backward compatibility

cm.core.calc

articleView.cm

The scope of the field additionalAdjustments has been changed, calling additionalAdjustments will instead return a deep copy of the array.

Old: package AdditionalGlobalPartAdjustment[] additionalAdjustments : public readable;
New: package AdditionalGlobalPartAdjustment[] _additionalAdjustments;

The following functions have been added to allow easier manipulation of the additional adjustments of an article view:

Added: final public AdditionalGlobalPartAdjustment[] additionalAdjustments()
Added: extend public void appendAdditionalAdjustments(AdditionalGlobalPartAdjustment[] adjustments)
Added: final public void clearAdditionalAdjustments()

For reference the other function to manipulate additional adjustments are:

extend public AdditionalGlobalPartAdjustment getAdditionalAdjustment(str key)
extend public void addAdditionalAdjustment(AdditionalGlobalPartAdjustment adjustment)
extend public void putAdditionalAdjustments(AdditionalGlobalPartAdjustment[] adjustments)
extend public void removeAdditionalAdjustment(AdditionalGlobalPartAdjustment adjustment)

class DrawingSettingsControlPanelPage

A new control panel page has been added titled Drawing Settings. As of 16.5, it only contains one setting for toggling article code display for Ind Tags in 2D/3D views. It's purpose is to hold settings that pertain to the drawing and are not user settings. A user setting, for example, would be the language setting that does not pertain to a single drawing but will persist across all user drawings.

The new DrawingSettingsControlPanelPage is registered during initialization of the cm.core.calc package in init.cm.

cm.core.calc.test.orderExport

class BaseCalcInfoPrinter

The printInfoTree function has been updated to print BasicPartInfoTreeColumns.

final package void printInfoTree(Part part, PartInfoTree info, PartListRow row, PartColumn[] viewColumns, int n=0) {
	...
	for (column in viewColumns, index=col) {
		for (col in info.columns) {
			if (col.eq(column)) {
				str output;
				
				if (col as PartInfoColumn) {
					...
New:			} else if (col as BasicPartInfoTreeColumn) {
New:				output = col.output(info, mainSpace());
				} else if (col as BasicPartColumn) {
					...
				}

				...
			}
		}
	}
	...
}

cm.core.collabG3

cm/core/realtime.cm

RTInvalidate calls that were invoked before the RT runtime is initialized are now deferred and will be invoked when the runtime is ready later. Previously, attempts that were made to invalidate certain CollabPro realtime synchronized data before RT runtime is ready will be lost, resulting in data inconsistencies.

See also related changes in cm.network.cbb.

cm.core.dwg

class CapPart

Constructor Migration: List Price → Base Price + Option Price Sum

A new constructor has been introduced for creating parts that takes basePrice and optionPriceSum directly. The old constructor that takes a single listPrice.

Old → New behavior

Old:

  • Constructor accepted a single listPrice along with articleCode, description, company, and catalog.
  • The list price combined both base price and option pricing into one value.
  • No separation between core part value and option upcharges.

New:

  • A new constructor separates basePrice (core part price) and optionPriceSum (sum of option prices).
  • Old listPrice constructor remains available but is annotated as deprecated
Impact
  • Modern pricing system alignment: Supports the new pricing model introduced in 16.5, where listPrice is derived from basePrice + optionPriceSum.
  • Clearer semantics: Separating base and option prices enables more accurate pricing breakdowns and better handling of specials/discounts.
  • Migration required:
    • Code currently using the listPrice constructor should migrate to the basePrice + optionPriceSum version.
  • Backward compatibility: Both constructors exist for now, ensuring older code compiles, but new development should adopt the new signature.

cm.core.geometry2D.advanced

public int insideCount(point2D dp, vector2D direction, double r=0) { is used to determine whether a point2D lies inside or outside the shape. Previously it would in rare cases indicate that it was inside while clearly outside and vise versa. It would also not return consistent results when the point was on the edge of the shape.

For example, a pyramid-shaped triangle with its tip pointing to the right would consider a point one meter to the right of its tip to be inside of the triangle. This is no longer the case.

This change is not likely to cause issues in common use cases. However, it can be good to smoke test functionality that heavily relies on APath2D or its derivatives.

cm.core.itemTag

class ItemTag

The invalidate(..) function has been updated to call info.?invalidate().

/**
 * Invalide the tag info.
 */
public void invalidate(dirty2D flag) {
New:	info.?invalidate();
	if (owner and owner.space) {
		space.invalidate(this, flag);
	}
}

A number of functions have been moved from the itemTag.cm file to the more appropriate functions.cm and hooks.cm files. These are the moved functions:

// Moved to functions.cm
public void enableItemTagsAlwaysUpdate(bool on) {}
public bool itemTagsAlwaysUpdateEnabled() {}
public void removeItemTags(Snapper this) {}

// Moved to hooks.cm
public bool updateWorldItemTags(World world, bool validate, PriceChangeEnv env) {}
public bool updateSpaceItemTags(Space space, bool validate, function():bool interrupt, bool force) {}
public void updateSpaceItemTags(Space space) {}

class ItemTagInfo

The buildGraph() function has changed to build the text based on the new setting value (explained in the ItemTagInfo compile-time section). It now calls getTagText() where it used to just get the tagText field:

NOTE: Any overrides of this function should utilize the new getTagText() function.

/**
 * Build the graph in local coordinates.
 */
extend public void buildGraph() {
	if (tStyle) {
		GText t = GText((0, 0), getTagText(), middle, tStyle);
		graphCache = t;
	} else {
		graphCache = GText((0, 0), getTagText(), middle, h=textHeight(), alwaysReadable=true);
	}
}

cm.core.part

Invisible Pricing Propagation Behavior

The following functions were added or modified in cm/core/part/parts.cm in v16.5. These functions were modified in support of the new pricing model which is documented in the compile-time section for cm.core.part.Part.

Changed: private void propagatePrice(Part p) {}
Added: private void propagatePriceNew(Part parent) {}

Changed: private void propagatePrice(Part p)

With the introduction of the new pricing system, the behavior of propagatePrice in cm/core/part/parts.cm has undergone changes.

Old → New behavior

Old:

  • Propagation used cached listPrice only.
  • When a child Part had invisiblePricing() == true:
    • Its totalListPrice() was divided by the parent’s quantity and added to the parent’s list price.
    • The child’s listPrice was set to 0.
    • Currency conversion was handled manually during this aggregation.

NEW:

  • propagatePrice now branches on the parent’s pricing model:
    • If the parent uses new pricing:
      • propagatePriceNew(..) is called
  • NOTE: Child parts are assumed to be utilizing the same pricing model as their parent Part

Added: private void propagatePriceNew(Part parent) {}

If the parent Part has useNewPricing() set to true, the new propagation system is utilized on the parent and child Parts. With the new pricing model, invisible pricing propagation is split into two: base price and option price sum.

Old → New behavior

Old:

  • Propagation is done in propagatePrice(..) and utilizes cached listPrice only.

NEW:

  • propagatePrice now branches on the parent’s pricing model:
    • If the parent uses new pricing:
      • Propagation is split into two dimensions:
        • Base price (totalBasePrice)
        • Option price sum (totalOptionPriceSum)
      • Each is separately aggregated from invisible children and divided by the parent’s quantity
      • Child data values are cleared
      • The parent’s cached data values are incremented accordingly
      • Currency conversion is no longer done here
        • Price getters now have translatePrice flag which is set to false during aggregation
  • NOTE: Child parts are assumed to be utilizing the same pricing model as their parent Part

class PartSpecial

Added: extend public void copy(PartSpecial special) {}

  • Allows for copying values from one PartSpecial instance to another
Behavior
  • Sets values on this to values from passed in special
  • Fields transferred:
    • str partNum
    • str descr
    • bool priceReplace
    • double amount

class PartGridBuilder

Constructor Change

Old → New behavior

Old:

  • Simply assigned the provided env.
  • If env was null, the instance had no default environment.
Old:
public constructor(PartGridBuilderEnv env=null) {
    this.env = env;
}

New:

  • If no environment is passed, it now creates a default PartGridBuilderEnv with buildTotalsRow = true.
  • Ensures a baseline configuration is always available, even without explicit input.
New:
public constructor(PartGridBuilderEnv env=null) {
    this.env = env ?? PartGridBuilderEnv(buildTotalsRow:true);
}

Added: extend public void populateGridWindow(Object data, GridWindow grid, PartGridBuilderEnv env=null) {}

  • High-level entry point to populate a GridWindow with arbitrary data.
Behavior
  • If no grid is provided → safely returns
  • If no env is passed → falls back to cached `this.env
  • Calls two helpers:
    • populateColumns(grid, env)
    • populateRows(data, grid, env)
  • Net effect: Wraps the column + row population into a single call.

Added: extend public void populateRows(Object data, GridWindow grid, PartGridBuilderEnv env=null)

  • Populates rows in a GridWindow from different types of input collections.
Behavior
  • If no grid → safely returns.
  • Falls back to cached this.env if no env is given.
  • Supports two input types:
    • Seq : Iterates and calls populateRow(obj, grid, env) for each.
    • Set: Same iteration and per-object row population.
  • Skips null objects
  • If env.buildTotalsRow == true, appends a totals row after populating all others.

class Part

Modified Pricing Behavior (v16.5 and later)

The following functions were modified with the introduction of the new pricing system (documented in cm.core.part.Part compile-time section).

Changed: final public double listPrice(bool includeChildren=false, Space space=null) {}
Changed: final public double listPrice(..)
Old → New behavior

Old:

  • If applicable, returns list price adjusted value
  • Otherwise, returns customListPrice(..)

New:

  • If applicable, returns list price adjusted value
  • Otherwise, checks useNewPricing() to determine pricing behavior
    • Returns calculatedListPrice(..) when useNewPricing is true
    • Returns customListPrice(..) when useNewPricing is false
Impact
  • See new pricing API documentation in cm.core.part.Part compile-time section for migration tips

Modified PartSpecials Behavior (v16.5 and later)

Retrieval and modification of PartSpecials on Part has been updated to account for flattened parts and to allow optional invalidation of world price.

Added: extend public PartSpecial getSpecial(str id, PropObj s=null) {}
Old → New behavior

Old:

  • Only supported fetching a PartSpecial using the implicit specialsKey().

New:

  • Adds an overload to get a special by an explicit id.
  • If no id is provided (empty string), it safely returns null.
  • Broadens lookup options: you can now fetch by specialsKey() or a direct identifier.
Changed: extend public bool containsSpecial(PropObj s=null) {}
Old → New behavior

Old:

  • Checked only the current owner for a special by calling getSpecial(s)

New:

  • Still checks the current owner
  • Additionally iterates through all owners, returning true if any owner has a special.
  • Result: containsSpecial now considers both self and inherited/related owners instead of just one.
Changed: putSpecial functions
Old: extend public void putSpecial(PartSpecial special, PropObj s=null) {}
New: extend public void putSpecial(PartSpecial special, PropObj s=null, bool invalidateWorldPrice=true) {}
New: extend public void putSpecial(str id, PartSpecial special, PropObj s=null, bool invalidateWorldPrice=true) {} 
Old → New behavior

Old:

  • Simple: placed a PartSpecial in the current owner’s PartSpecialHolder keyed by specialsKey()
  • Always invalidated world price.

New:

  • Signature extended with bool invalidateWorldPrice = true
  • Now supports two ways of saving:
    • If the passed in PropObj s=null parameter is not null → s.putSpecial(...)
    • Otherwise → iterates over all owners and updates them.
  • Actively propagates invalidateWorldPrice flag.
  • Ensures consistency across multiple owners, not just the direct one.
Changed: removeSpecial functions
Old: extend public void removeSpecial(PropObj s=null) {}
New: extend public void removeSpecial( PropObj s=null, bool invalidateWorldPrice=true) {}
New: extend public void removeSpecial(str id, PropObj s=null, bool invalidateWorldPrice=true) {
Old → New behavior

Old:

  • Removed a special only from the current owner using specialsKey()
  • Always invalidated world price

New:

  • Two overloads:
    • One that removes by explicit id
    • One that removes by the derived specialsKey()
  • Both propagate the invalidateWorldPrice flag
  • Iterates through all owners, not just the direct owner
  • More robust cleanup — ensures that the special is removed everywhere it might exist

Other Part Changes

Added: extend public str annotationFlattenableKey() {}

With the addition of the new part attribute description/notes system, Parts now need to be differentiable/split by their attribute values.

The following helper function to generate the flattenable key for attributes has been made. It is appended to the flattenable key in flattenableKey():

/**
 * Flattened annotation note/descs key.
 * @return str of flattenable key of PartAnnotation(s)
 */
extend public str annotationFlattenableKey() {
	StrBuf buf();
	bool first = true;
	for (annotation in getAnnotations()) {
		if (first) first = false;
		else buf << ',';
		buf << annotation.key();
	}
	return buf.retireToS();
}
Changed: extend public str flattenableKey() {}

With the addition of the new part attribute description/notes system, Parts now need to be differentiable/split by their attribute values.

The following change has been made in flattenableKey to account for this:

/**
 * Flattenable key.
 */
extend public str flattenableKey() {
	StrBuf key;
	
	...
	
New:	key << annotationFlattenableKey();
	
	return key.any() ? key.retireToS() : articleCode();
}
Changed: extend public Object valueOf(PartColumn column) {}

With the addition of the new part attribute description and note columns in Calculations, the valueOf(..) function has been updated in Part to provide values for these columns.

This is mainly useful for the Excel order export and exporting a single, comma-delimited, str value for PartAttributeColumns.

/**
 * Normally returned from value method in 'column', this is a change to override it from part.
 */
extend public Object valueOf(PartColumn column) {
	if (column as PartAttributeColumn) {
		?str[] colVals = column.value(this, null);
		if (!colVals) return null;

		StrBuf valuesBuf();
		bool first = true;
		for (val in colVals) {
			if (first) first = false;
			else valuesBuf << ", ";
			valuesBuf << val;
		}

		return valuesBuf.retireToS();
	}
	return null;
}

Changed: extend public void updateItemTags() {}

With the addition of the new str articleCodeText field on ItemTagInfo, Part now sets this value during updateItemTags() if it is not already set. This ensures that the field has a value and that the new Control Panel setting for toggling article codes for Ind Tags is functional in most cases.

/**
 * Update the Item tags.
 */
extend public void updateItemTags() {
	...
	
	if (owner and acceptItemTags()) {
		if (ItemTags tags = owner.itemTags()) {
			if (ItemTag tag = tags.get(itemTagKey())) {
				...
	
New:			if (tag.info and !tag.info.articleCodeText) {
New:				tag.info.articleCodeText = articleCode();
New:				invalidate = true;
				}
				
				...
				return;
			}
		}
		...
	}
}

class FlattenedPartData

New Part-To-Owner Interface

A new interface, partToOwnerKeys, has been added to FlattenedPartData. These keys are str values that are unique for each Part within a single owner. Their purpose is to distinguish between multiple Parts on the same Snapper owner that may share the same flattenableKey but originate from different creators.

Example

Consider a table Snapper with four legs:

  • When processed, the table generates one Part for the tabletop and four Parts for the legs.
  • During part merging, if all four legs are identical, they may be flattened into a single leg Part.
  • The resulting FlattenedPartData contains an owners sequence pointing only to the table Snapper.
Previous Limitation

In this scenario, the flattened part had no way to reference the four individual leg parts that were merged together—it only knew about the shared owner.

New Behavior

The partToOwnerKeys interface resolves this gap by assigning unique keys for each contributing Part. This allows the flattened part to retain identifiers for the individual parts that generated it, ensuring traceability even after merging.

FlattenedPartData Modifications

To accomplish this, FlattenedPartData has a new field where the keys are stored (private str{} _partToOwnerKeys), a new accessor function (final public str{} partToOwnerKeys()), and a change in appendSimilar to build the keys. The appendSimilar function's purpose is to append a new Part to the flattened part info. It increases quantity, appends owners, and changes the level of the flattened part. It now also appends the passed in Part parts partToOwnerKey into the _partToOwnerKeys sequence.

In cm/core/part/flattenedPartData.cm 


New: private str{} _partToOwnerKeys;
New: final public str{} partToOwnerKeys() {}

Changed:
/**
 * Append a similar part.
 */
final public void appendSimilar(Part part, double multiplier) {
	appendOwnersFrom(part.data);

New:	if (!_partToOwnerKeys) init _partToOwnerKeys();
New:	_partToOwnerKeys << part.partToOwnerKey();

	_quantity += part.quantity()*multiplier;

	level = min(level, part.level());

	#if (!builtInReleaseMode) assert(_allowOtherParent == part.data.allowOtherParent);
}

class PartAnnotationHolder

See cm.core.part.attributes section in 16.5 New Features migration guide for new PartAnnotationHolder class documentation.

class PartGridBuilderEnv

Added: extend public Brush getCellBrush(Object data, int columnIdx) {}

  • Allows for customization of a row and columns cell background
  • Parameters:
    • Object data: Data for row to get cell brush for
    • int columnIdx: Column index to get cell brush for
Behavior
  • Returns whiteBrush by default

Added: extend public GridCell getDefaultCell(Object data, int columnIdx) {}

  • Gets default cell for row and column
  • Parameters:
    • Object data: Data for row to get default cell for (can be null)
    • int columnIdx: Column index to get default cell for
Behavior
  • Returns ColorNameGridCell("", bgColor=getCellBrush(..).color) by default

Added: extend public GridCell getPartRowCell(Part part, int columnIdx) {}

  • Gets part row cell for Part and column
  • Parameters:
    • Part part: Part for row to get cell for (can be null)
    • int columnIdx: Column index to get cell for
Behavior
  • If Part is null or column index is out of bounds, returns getDefaultCell(..)
  • Otherwise, returns a cell for the Part data associated with the column index
    • Assigns a background color to the cell by calling getCellBrush(..)

Changed: extend public GridCell[] getPartRowCells(Part part) {}

Old → New behavior

Old:

  • Directly returned a sequence of GridCells

New:

  • Loops through columns, calling getPartRowCell for each column and appending it to the returned GridCell sequence
  • Allows for more flexibility when building row cells

class PartAnnotation

See cm.core.part.attributes section in 16.5 New Features migration guide for new PartAnnotation class documentation.

cm/core/part/functions.cm Special functions

  • The parameter bool invalidateWorldPrice=true has been added to special functions
    • Gives the option to invalidate world price upon making changes to specials
    • Previously, world price was invalidated on every special change
Old: public void removeSpecialHolder(PropObj s) {}
New: public void removeSpecialHolder(PropObj s, bool invalidateWorldPrice=true) {}

Old: public void putSpecial(PropObj s, str key, PartSpecial special) {}
New: public void putSpecial(PropObj s, str key, PartSpecial special, bool invalidateWorldPrice=true) {}

Old: public void removeSpecial(PropObj s, str key) {}
New: public void removeSpecial(PropObj s, str key, bool invalidateWorldPrice=true) {}

Old: public void removeAllSpecials(PropObj s) {}
New: public void removeAllSpecials(PropObj s, bool invalidateWorldPrice=true) {}

cm.core.part.query

class PartMakeSpecialDialog

Changed: extend public void onOKButtonClick(Object sender, Object args) {}

The event callback for the OK button click has been modified to utilize the new generateSpecial method. It now calls generateSpecial rather than creating a special on the fly.

Old vs. New Behavior

Old:

  • Created a new PartSpecial within the event callback
  • Can easily lead to duplicated code if customization is needed for the generated PartSpecial
Old:
extend public void onOKButtonClick(Object sender, Object args) {
	PartSpecial newSpecial(partNumTF.text,
						   descrTF.text,
						   priceReplaceRB.currentState > 0,
						   amountDF.value);
	... 
}

New:

  • PartSpecial generation has been separated out into a designated function generateSpecial
    • PartSpecial can be customized here
  • The OK button click callback can now call this function along with performing its other behaviors
New:
extend public void onOKButtonClick(Object sender, Object args) {
	if (PartSpecial newSpecial = generateSpecial()) {
		...
	}
	...
}

Added: public void selectAll() {}

  • A new override of the selectAll function was added in QueryGridWindow
  • It was overridden to include the column header row on select all events
    • super() does not select this row in selectAll

Behavior

  • If grids selection mode is multi-select (isUsingMultiSelect == true):
    • Clears the seq of multiSelectedRows
    • Adds all rows from column headers (-1) to row count - 1 to multiSelectedRows
    • Invalidates and flushes grid

class QueryDialogBehavior

Changed: extend public void onSpecialChanged(Object sender, Object args) {}

Specials created or modified in the QueryDialog no longer replace the stored PartSpecial value on the PartSpecialHolder. Instead, their values are copied over to the existing PartSpecial instance.

Old vs. New Behavior

Old:

  • PartSpecials created or modified in the QueryDialog fully replaced existing PartSpecial instances
extend public void onSpecialChanged(Object sender, Object args) {
	if (sender as Window) {
		...		
		if (args as QuerySpecialChangedEventArgs) {
			dialog.putSpecial(dialog.selectedRowID(), args.newSpecial); // directly replaces existing PartSpecial
		}
	}
}

New:

  • PartSpecials created or modified in the QueryDialog copy their values over to the existing PartSpecial instance
extend public void onSpecialChanged(Object sender, Object args) {
	if (sender as Window) {
		...		
		if (args as QuerySpecialChangedEventArgs) {
			PartSpecial original = args.oldSpecial;
			original.copy(args.newSpecial); 							// copies new values to existing PartSpecial

			putSpecial(dialog, dialog.selectedRow(), original);
		}
	}
}

class EditGridCell

Added: public str clipboardValue() {}

  • An override of the GridCell function clipboardValue() has been added in EditGridCell. It returns the cells outS() value.

cm.core.toolbox

Core settings for toolboxCurrentTab changes

Following the changes to replace the lastSelectedTab field with lastSelectedTabKey in tbInfo.cm, the last selected tab is now stored as a str instead of an int in core settings. Although the key remains unchanged as toolboxCurrentTab_<toolboxCardKey>, developers may need to modify the casting type when retrieving the key from an int to a str to accommodate for this change.

cm.core.user

class CoreDistanceField

The mouse over tooltip now always display the distance with the same precision as the field value itself. Previously, the tooltip would be shown with the precision of the currently selected "Dimensions" style from the Tools toolbox.

It is now also possible to specify the precision of CoreDistanceFields that are created from props, by setting the unitPrecision argument in the PropInputSettings. For example, to set the precision to be the same as the currently selected "Dimensions" style, use unitPrecision=defaultUserDimensionStylePrecision().

public class MyAnimation extends Animation {
    public props {
	    "length" : setting=PropInputSetting(unitPrecision=defaultUserDimensionStylePrecision());
    }
}

cm.import.sqlite

SQliteDB

SQLiteDb's abort() function is now rectified to execute ROLLBACK instead of COMMIT, which will erroneously commit unwanted changes to the DB.

cm.runtime

The reflection class Field has a method named copyShallow() that is intended to indicate whether or not that field is marked as copy=shallow. However, it was actually returning a value that correlated to the field being copy=null. As of 16.5, this has been corrected to return a value that matches the intented behavior.

cm.win

IconFinder

For icons resisiding in base/res/images/, CET will now return a DibImage instead of MemoryImage. This is to reduce reliance on GDI bitmap objects to display the CET UI. This only affects calls to icon that have their key "#default" or by calling dibIcon, such as:

icon("partTag")
icon("partTag", key=#default)

dibIcon("panel_frame.png", key=#fikaOffice);

For existing code that performed a cast check to MemoryImage, you have to migrate the code to also handle DibImage. Common issues are:

  • Images not loading
  • Disabled images are not grayed out (blend is not applied)

One such example is listed below:

// Previous logic only handles MemoryImage
byte beforeBlend = 255;
if (image as MemoryImage) {
    beforeBlend = image.blend;
    image.blend = 100;
}
image.transparentDraw(c.hdc, imgPos);
if (image as MemoryImage) image.blend = beforeBlend;

// New logic now handles MemoryImage, DibImage and SvgImage
byte beforeBlend = image.blend;
image.blend = 100;
image.transparentDraw(c.hdc, imgPos);
image.blend = beforeBlend;

Improved lifecyle of Images

In previous versions of CET, IconFinder would pass use=true when constructing an image. This would cause the Image to always be loaded as the image will never be destroyed (refCount never goes down to 0).

This has now been updated to not pass use=true, instead your dialog / control should be handling the use and release of the image. This is not required for ImagePainter as they already handle the use and release for you.

You may retrieve a terminated (blank) icon in some scenarios:

  • Retrieving an icon and directly drawing in your draw / repaint method.
  • Assigning an icon to a field and using it later.

A common pattern to fix this is to assign and use the icon on construction. Followed by releasing it on destruction (beforeRemove / removeWindow). This is used by BrushHoverDropDownMenuButton.

    /**
     * Second image.
     */
    private Image secondImage : package readable;


    /**
     * Constructor.
     */
    public constructor(Window parent, ...
                       Image secondImage=null, ...) {
        ...
        setSecondImage(secondImage, refresh=false);
        ...
    }


    /**
     * Set second image.
     */
    final public void setSecondImage(Image image, bool refresh=true) {
        if (this.secondImage) this.secondImage.release();
        this.secondImage = image;
        if (this.secondImage) this.secondImage.use();
        if (refresh) refresh();
    }


    /**
     * Before remove event, sent to all children (leaf first) before remove.
     */
    public void beforeRemove() {
        if (secondImage) secondImage.release();
        secondImage = null;
        super();
    }

Another example where we are not using a Window, but relying on finalizer for CustomTitleBar.

    /**
     * Right side icons.
     */
    private Image minimizeIcon;


    /**
     * Build a custom title bar using the config.
     */
    public constructor(FrameWindow parent, TitleBarConfig config) {
        ...
        minimizeIcon = dark ? icon("win/minimizeLight") : icon("win/minimizeDark");
        if (windowIcon) windowIcon.use();
        ...
    }


    /**
     * Retire if GCed.
     */
    private finalizer() {
        if (captionBrush) retire();
    }


    /**
     * Retire.
     */
    final public void retire() {
	    if (windowIcon) windowIcon.release();
	    windowIcon = null;
        ...
    }

Auto remember position and size

AppWindow and DialogWindow have the following overrides, subclasses now will automatically remember their last dialog position and size. If this causes issues with your dialog, you can override the methods in your own dialog to return false.

Old: public bool autoSavePos() { return false; }
New: public bool autoSavePos() { return true; }

Old: public bool autoSaveSize() { return false; }
New: public bool autoSaveSize() { return true; }

ComboTextPainter

setBound(rectI r) now attempts to reset its contained TextPainter width before autoSizing to make consistent initial width calculation when painting text. This potentially affects how text can be painted and truncated automatically.

class TextInputGridCell

Added: public bool keyTab() {}

Advances input focus from a TextInputGridCell to the next cell in the parent GridWindow on user tab-click.

/**
 * Key tab.
 */
public bool keyTab() {
	GridWindow gw = gw();
	if (gw) {
		gw.setFocus();
		gw.keyTab();
	}
	return true;
}

class GridWindow

Changed: extend public str clipboardValueIfAny(int x, int y) {}

  • The clipboardValueIfAny method in the grid was updated to handle MoneySumGridCell objects.

Changed: extend public int appendRow(str label=null, bool update=true) {}

appendRow(..) has been updated to call updateScrollBars() when the update parameter is true.

/**
* Append a new row and return the new row index.
*/
extend public int appendRow(str label=null, bool update=true) {
	...
	
	if (update) {
		updateRowSize(index);
		updateColumnSize(-1, updateRows=false);
New:	updateScrollBars();
		refreshG2();
	}

	...

	return index;
}