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.
runningInInstallAndCloseMode
).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:
ProductCatalog
(data.prdCatalog()
) before resolving the PricelistType
ProductCatalog
was found, the logic would not proceed to pricing.New:
ProductCatalog
check has been removed. PricelistType
exists. addPriceFactors
.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) {}
Indicates whether the new part pricing system should be used when generating Parts. An opt-in feature flag to the new pricing system.
getParts
, and createPart
.Calculates the total option prices from a collection of SpecOption
s. Optionally takes a Part
parameter to allow for special pricing scenarios.
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:
createPart(Snapper snapper, PartsEnv env, double price)
useNewPartPricing
is falseNew:
createPart(Snapper snapper, PartsEnv env, double basePrice, Double optionPriceSum)
basePrice
and optional optionPriceSum
useNewPartPricing
is truebasePrice
and optionPriceSum
The getParts
method was updated to integrate the new pricing model.
Old:
price(options)
createPart(..., price)
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:
bool newPricing = useNewPartPricing();
basePrice()
, and new createPart
uses new signatureprice(options)
, and createPart
uses legacy signatureinvalidateOptionPriceSum()
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); ... } }
useNewPartPricing()
is disabled.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; }
The logic for initializing a pricelistRef
was simplified by replacing an early return with direct initialization when the reference is empty.
Old:
pricelistRef
was already set, the method returned immediately and did nothing.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:
pricelistRef
is empty, it is set to the current currency symbol.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)); }
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:
createPart
method signature:
public DsPart createPart(Snapper snapper, PartsEnv env, double price)
getParts
when useNewPartPricing
is false New:
createPart
method signature:
public DsPart createPart(Snapper snapper, PartsEnv env, double basePrice, Double optionPriceSum)
getParts
when useNewPartPricing
is truelistPrice
to passing basePrice
and optionPriceSum
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:
DsPart dpart = createPart(snapper, env, freeformItem.price());
Old: public void getParts(Snapper snapper, PartsEnv env) { ... if (usePartsCache and cachedParts.any) { ... } else { DsPart dpart = createPart(snapper, env, freeformItem.price()); ... } }
New:
useNewPartPricing()
:
basePrice
(and optionPriceSum = null
).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()); } ... } }
basePrice
or price
, depending on configuration.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:
super(snapper, fpData, fpData.freeformItem.price(), qty);
New:
useNewPartPricing()
:
basePrice
and optionPriceSum=null
.listPrice
as before.listPrice
or basePrice
depending on pricing model configuration.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) {}
Allows proxy-aware code to check whether the new pricing system is enabled.
A new overload of optionPriceSum
was introduced to support execution through a DsPDataProxy
, enabling proxy-aware option pricing calculations.
blockDataProxy
to prevent recursive proxy calls.dsEnv.data.optionPriceSum(opts, part)
.blockDataProxy
is decremented in a finally block for safety.DsPDataProxyEnv
).optionPriceSum(SpecOption[], Part)
method.DsPDataProxy
is available to ensure correct delegation.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:
createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double price)
useNewPartPricing
is falseNew:
createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double basePrice, Double optionPriceSum)
useNewPartPricing
is truecreatePart
behavior in the new pricing system.The readProduct
method was refactored to read the I1 SIF value during product import.
Old:
DsFreeformItem
sNew:
DsFreeformItem
base price propertyThe readOptions
method was refactored to read the O1 SIF value during product option import.
Old:
DsFreeformItem
sNew:
DsFreeformItem
price property for options/featuresThe 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) {}
An override of basePrice
has been added to OFMLPData
to support the migration path to the new pricing model (≥16.5).
Old:
New:
useNewPartPricing()
→ returns only B-level row value from the pricelist.price()
method).The specOption(..)
function has been overridden in OFMLPData
to support pricing on an OFMLPart
s SpecOption
s.
Old:
specOption()
never adjusted option pricing directlyNew:
setOptionPrice()
Added as a helper to the overridden specOption(..)
function for setting SpecOption
price.
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:
createPart(Snapper snapper, PartsEnv env, double price)
useNewPartPricing
is falseNew:
createPart(Snapper snapper, PartsEnv env, double basePrice, Double optionPriceSum)
basePrice
and optional optionPriceSum
useNewPartPricing
is truebasePrice
and 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:
createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double price)
useNewPartPricing
is falseNew:
createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double basePrice, Double optionPriceSum)
useNewPartPricing
is truecreatePart
behavior in the new pricing system.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:
double price = dsEnv.data.price(options);
SpecOption[]
options were always passed into price()
to calculate the full part cost.New:
basePrice
directly.optionPriceSum
when calling createPart()
.price(options)
as before.getPartsInternal()
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:
createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double price)
useNewPartPricing
is falseNew:
createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double basePrice, Double optionPriceSum)
useNewPartPricing
is truecreatePart
behavior in the new pricing system.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.
cm.core.part.Part
compile-time section for more migration tipsNew : public str key;
New: public guid gid; New: public str{} snapperGidSet(); New: public KitchenAutoMeasure->Snapper measureSnapperMap() : copy=null; New: public symbol spaceVolumeId;
New: final public void generateGid(Int idx=null) {
New: extend public bool allowAutoElevations() : null=false {
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:
double price = dsEnv.data.price(options);
SpecOption[]
options were always passed into price()
to calculate the full part cost.New:
basePrice
directly.optionPriceSum
when calling createPart()
.price(options)
as before.getPartsInternal()
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:
createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double price)
useNewPartPricing
is falseNew:
createPart(DsPDataProxyEnv dsEnv, PartsEnv env, double basePrice, Double optionPriceSum)
useNewPartPricing
is truecreatePart
behavior in the new pricing system.The COMPart
constructor was updated to explicitly separate base price and option price sum when initializing the part.
Old:
pricePerUnit()
) into the superclass constructor.New:
basePrice=pricePerUnit()
and optionPriceSum=0
to the superclass.COMPart
creation with the new part pricing systemCOMPart
now integrates with the new pricing model that distinguishes base vs option costs.pricePerUnit()
) is preserved since option sum is initialized as 0.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.
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
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); } }
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(..); } }
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; } }
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); } }
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); } } }
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) { } }
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.
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 Part
s export.
It loops through the part
s 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); } } }
Old:
Old: public constructor(PartGridBuilderEnv env=null) { this.env = env; }
New:
New: public constructor(PartGridBuilderEnv env=null) { this.env = env ?? ProdPartGridBuilderEnv(buildTotalsRow=true); }
See cm.abstract.part
section in 16.5 New Features migration guide for new CustomSpecOption
class documentation .
AbsPart
now returns a SpecOptionInfoTree
by default when generating info treesextend public PartInfoTree createInfoTree(SpecOption option, PartInfoTree parent=null) { Old: return null; New: return SpecOptionInfoTree(this, option, parent=parent); }
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) {}
The AbsBasePricePartColumn
value now calls the new basePrice(..)
function on core Part
.
Old:
AbsPart
part.basePrice()
if soNew:
basePrice(..)
function value directly from core PartThe AbsUpchargePartColumn
value now calls the new optionPriceSum(..)
function on core Part
.
Old:
AbsPart
part.upcharge()
if soNew:
optionPriceSum(..)
function value directly from core PartOld:
NameGridCell
with the value New:
item
is a SpecOptionInfoTree
, returns a SpecOptionNameGridCell
with the item
s associated value, part, and optionNameGridCell
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); }
See cm.abstract.part
section in 16.5 New Features migration guide for new CustomOptionSpecial
class documentation .
Old:
_columns
field was set, its value was returnedsuper()
was returned (which is null)Old: public PartColumn[] columns() { return _columns ?? super(); }
New:
_columns
field was set, its value was returnedSpecOption
columns
specOptionInfoColumn
(option code)specOptionDescColumn
(option description)specOptionUpchargeColumn
(option price)New: public PartColumn[] columns() { return _columns ?? [PartColumn: specOptionInfoColumn, specOptionDescColumn, specOptionUpchargeColumn]; }
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.
constructor(Snapper snapper, str articleCode, str description, double listPrice, …)
constructor(Snapper snapper, str articleCode, str description, double basePrice, Double optionPriceSum, …)
cm.core.part.Part
compile-time section for more migration tipsIntroduces a new method to compute the total option price, explicitly aggregating all SpecOption upcharges, including specials.
specOptions()
.upcharge(this)
for contextual pricing.generateOptionPriceSum()
in your extended Part class.invalidateOptionPriceSum()
) when option state changes to trigger regeneration.The invalidateSpecOptions
method is introduced alongside the new SpecOptions caching mechanism.
invalidateOptionPriceSum
is true (default), also clears the cached option price sum.invalidateSpecOptions()
to prevent stale valuesrebuildSpecOptions
documentation)invalidateSpecOptions()
in those paths to ensure cache consistency.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:
invalidateOptionPriceSum()
is triggered to ensure the cache stays correctinvalidateOptionPrice=false
to skip cache invalidation if they plan to manage it manually or delay recalculation.With the introduction of custom options in the query dialog, the following changes were made in ProdPart
:
specOptions()
now inserts custom options
CustomSpecOption
s sequence
valuesequence
is -1 (default for CustomSpecOption
)nonCustomSpecOptions()
SpecOption
s except CustomSpecOption
sCustomSpecOption
instances while collectingcustomSpecOptions()
CustomSpecOption
s only.PartSpecialHolder
lookup to gather user-defined options.SpecOption
s
private SpecOption[] _specOptions : stream=null
_partOptions
changesspecOptions()
now cached
_specOptions
, rebuilds only if null, otherwise returns cached.rebuildSpecOptions()
SpecOption
sequence rebuilding CustomSpecOption
s) in addition to standard onesAdded: 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) {}
With the addition of the new Attribute Note and Description columns in the Calculations dialog, the following changes have been made to ProdPart
:
partToOwnerKey
function
partSourceID
value appended to the super()
valuegenerateSifAnnotationRows
function
Part
s PartAnnotation
sPartAnnotation
PartAnnotation
s note valuePartAnnotation
s description valuegenerateAttributeData
function
Part
s PartAnnotation
sAttributeData
objects for each PartAnnotation
AttributeData
objects to the ItemData
s attributes
sequenceinitializeAnnotationsFromItemData
ItemData
s attributes
sequence PartAnnotation
objects from the AttributeData
sadds the
PartAnnotationobjects to the
Part`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) {}
A new SpecOptionUpchargeInfoTreeColumn
has been added to display price values associated with SpecOption
s in a PartInfoTree
SpecOptionNameGridCell
The helper method generateSpecial()
has been overridden to streamline the process of creating OptionSpecial
objects from dialog input.
OptionSpecial
type, calls and returns generateOptionSpecial
valuesuper()
valueThe helper method generateOptionSpecial()
has been created to create OptionSpecial
objects from dialog input.
CustomOptionSpecial
type, creates and returns a CustomOptionSpecial
made from the dialogs input valuesOptionSpecial
given the dialogs input valuesIn 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.
SpecOption
and it's parent Part
SpecOption
s special on the owning Part
public QueryRowData parent;
)./** * 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() { ... } }
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() { ... } }
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.
putSpecial
:
CustomOptionSpecial
s to be created when a ProdPart
row is selected in the query dialog/** * 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(..); } } }
As of 16.5, a new interface for lead time and PartAnnotation
s 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); ... } ... }
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
.
saveInfo
fieldbool update
(default: true) parameterupdateDependent
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); }
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 { ... } }
saveInfo
back to default false value after close saveInfo
to false before calling super()
Text fields created in this function (FormattedTextField
) now set the enterKeyCallback
parameter to contentChangedCB
rather than applyCB
Old:
FormattedTextField
s used applyCB
as their enter-key callbackapplyCB
called window.apply()
, automatically applying changes to the world cached project information objectNew
FormattedTextField
s now use contentChangedCB
as their enter-key callbackProjectInformationDialog
should now only update the world cached ProjectInformation
when the user selects "OK" in the dialogDate fields created in this function (DateField
) now set the callback
parameter to contentChangedCB
rather than applyCB
Old:
DateField
s used applyCB
as their callbackapplyCB
called window.apply()
, automatically applying changes to the world cached project information objectNew
DateField
s now use contentChangedCB
as their callbackProjectInformationDialog
should now only update the world cached ProjectInformation
when the user selects "OK" in the dialogwindow.textContentChanged()
with update
parameter set to falseOld:
window.textContentChanged
with update
parameter set to default (true)contentChangedCB
callback would automatically update the world cached project informationNew:
window.textContentChanged
with update
parameter set to falsecontentChangedCB
callback do not update world project information automaticallyProjectInformationDialog
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); } }
Now calls window.enableSelected()
with update
parameter set to false
Old:
window.enableSelected
with update
parameter set to default (true)checkBoxContentChangedCB
callback would automatically update the world cached project informationNew:
window.enableSelected
with update
parameter set to falsecheckBoxContentChangedCB
callback do not update world project information automaticallyProjectInformationDialog
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); } }
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
.
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; ... }
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
.
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
.
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 has been adjusted to avoid trySnap()
from being inadvertently called twice within the same action to improve performance.
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) {
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); } ... } }
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.
constructor(Snapper snapper, str articleCode, str description, double listPrice, ...)
listPrice
value to cache.cm.core.part.Part
for more infoconstructor(Snapper snapper, str articleCode, str description, double basePrice, Double optionPriceSum, ...)
basePrice
and optionPriceSum
separately for caching.cm.core.part.Part
for more infobasePrice
and optionPriceSum
.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)
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
.
The printInfoTree
function has been updated to print BasicPartInfoTreeColumn
s.
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) { ... } ... } } } ... }
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.
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:
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.
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) {}
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); } }
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) {}
With the introduction of the new pricing system, the behavior of propagatePrice
in cm/core/part/parts.cm
has undergone changes.
Old:
listPrice
only.invisiblePricing() == true
:
totalListPrice()
was divided by the parent’s quantity and added to the parent’s list price.listPrice
was set to 0.NEW:
propagatePrice
now branches on the parent’s pricing model:
propagatePriceNew(..)
is calledIf 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:
propagatePrice(..)
and utilizes cached listPrice
only.NEW:
propagatePrice
now branches on the parent’s pricing model:
totalBasePrice
)totalOptionPriceSum
)translatePrice
flag which is set to false during aggregationPartSpecial
instance to anotherthis
to values from passed in special
str partNum
str descr
bool priceReplace
double amount
Old:
Old: public constructor(PartGridBuilderEnv env=null) { this.env = env; }
New:
New: public constructor(PartGridBuilderEnv env=null) { this.env = env ?? PartGridBuilderEnv(buildTotalsRow:true); }
GridWindow
with arbitrary data.populateColumns(grid, env)
populateRows(data, grid, env)
GridWindow
from different types of input collections.populateRow(obj, grid, env)
for each.env.buildTotalsRow == true
, appends a totals row after populating all others.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) {}
Old:
customListPrice(..)
New:
useNewPricing()
to determine pricing behavior
calculatedListPrice(..)
when useNewPricing
is truecustomListPrice(..)
when useNewPricing
is falsecm.core.part.Part
compile-time section for migration tipsRetrieval and modification of PartSpecial
s on Part
has been updated to account for flattened parts and to allow optional invalidation of world price.
Old:
PartSpecial
using the implicit specialsKey()
.New:
Old:
New:
containsSpecial
now considers both self and inherited/related owners instead of just one.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:
PartSpecial
in the current owner’s PartSpecialHolder
keyed by specialsKey()
New:
bool invalidateWorldPrice = true
PropObj s=null
parameter is not null → s.putSpecial(...)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:
specialsKey()
New:
specialsKey()
With the addition of the new part attribute description/notes system, Part
s 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(); }
With the addition of the new part attribute description/notes system, Part
s 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(); }
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 PartAttributeColumn
s.
/** * 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; }
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; } } ... } }
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 Part
s on the same Snapper
owner
that may share the same flattenableKey
but originate from different creators.
Consider a table Snapper
with four legs:
Part
for the tabletop and four Part
s for the legs.Part
.FlattenedPartData
contains an owners sequence pointing only to the table Snapper.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.
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.
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 part
s 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); }
See cm.core.part.attributes
section in 16.5 New Features migration guide for new PartAnnotationHolder
class documentation.
Object data
: Data for row to get cell brush forint columnIdx
: Column index to get cell brush forwhiteBrush
by defaultObject data
: Data for row to get default cell for (can be null)int columnIdx
: Column index to get default cell for ColorNameGridCell("", bgColor=getCellBrush(..).color)
by defaultPart part
: Part for row to get cell for (can be null)int columnIdx
: Column index to get cell for Part
is null or column index is out of bounds, returns getDefaultCell(..)
getCellBrush(..)
Old:
GridCell
s New:
getPartRowCell
for each column and appending it to the returned GridCell
sequenceSee cm.core.part.attributes
section in 16.5 New Features migration guide for new PartAnnotation
class documentation.
bool invalidateWorldPrice=true
has been added to special functions
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) {}
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:
PartSpecial
within the event callbackPartSpecial
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 hereNew: extend public void onOKButtonClick(Object sender, Object args) { if (PartSpecial newSpecial = generateSpecial()) { ... } ... }
QueryGridWindow
super()
does not select this row in selectAll
isUsingMultiSelect
== true):
multiSelectedRows
multiSelectedRows
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:
PartSpecial
s created or modified in the QueryDialog
fully replaced existing PartSpecial
instancesextend 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:
PartSpecial
s created or modified in the QueryDialog
copy their values over to the existing PartSpecial
instanceextend 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); } } }
GridCell
function clipboardValue()
has been added in EditGridCell
. It returns the cells outS()
value.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.
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 CoreDistanceField
s that are created from props, by setting the unitPrecision
argument in the PropInputSetting
s. 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()); } }
SQLiteDb's abort()
function is now rectified to execute ROLLBACK
instead of COMMIT
, which will erroneously commit unwanted changes to the DB.
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.
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:
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;
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:
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; ... }
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; }
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.
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; }
clipboardValueIfAny
method in the grid was updated to handle MoneySumGridCell
objects. 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; }