cm.abstract.conveyors

Conveyor side connectors can initiate snapping

Previously, acConvSideRule had no snap type, so conveyor side connectors could receive connections but could not initiate snapping. The rule now includes ACConveyorDischargeCT, ACConveyorChargeCT, and ACConveyorDivertCT as snap types. This allows a conveyor's side connector to snap onto charge, discharge, and divert connectors when moving or inserting the conveyor.

The divert type allows side connectors to snap onto spur connectors using acConvWedgeRule. Without it, a spur could snap onto a conveyor side, but moving the conveyor side onto the spur could not establish the connection.

Old:
acConvSideRule = makeConnectRule("absConvSideRule", null, ACConveyorSideChannelCT);

New:
acConvSideRule = makeConnectRule("absConvSideRule",
                                 {ConnectTypeClass: ACConveyorDischargeCT,
                                                    ACConveyorChargeCT,
                                                    ACConveyorDivertCT},
                                 {ConnectTypeClass: ACConveyorSideChannelCT});

Extensions using acConvSideRule inherit this behavior automatically. The rule ID absConvSideRule and attach type ACConveyorSideChannelCT are unchanged.

cm.abstract.materialHandling

Construction engine entry cache key

The cacheKey() of MhEngineConstructionEntry has been updated to also include the additionalShapeArgs() map. This change was made as entries which were equal in dimensions and classifications but had a different additionalShapeArgs() would end up only using one of the entries to generate snappers as they shared the same cacheKey(). An example of this are levels that are identical except that they reference unit loads with different materials (an the unit load reference were stored in the additionalShapeArgs()).

Additionally, the generated cache key is now converted into a 32-character hash string to reduce the string length for further optimzation of key comparisons.

public class MhEngineConstructionEntry extends MhEngineSnapperEntry {

Old:
    public str cacheKey() {
        if (_cacheKey.any) return _cacheKey;
        return _cacheKey = concat(class.id.hex, classification.id.toS, localBound.toS);
    }


New:
    public str cacheKey() {
        if (_cacheKey.any) return _cacheKey;
        return _cacheKey = initCacheKey();
    }


    extend public str initCacheKey() {
        str res = concat(class.id.hex, classification.id.toS, localBound.toS);

        // Add additional shape args to cache key.
        str argsKey = cacheKeyArg(additionalShapeArgs());
        if (argsKey.any) res = concat(res, ";args=", argsKey);

        // Convert to 32-char hash.
        res = Sha256(res).hex().substring(0, 31);

        return res;
    }
    
    
    extend public str cacheKeyArg(Object obj, int depth=0) {
        if (depth >= 32) {
            if (developMode) ptrace("Too deep cache key"; #obj; #depth);
            return null;
        }
        if (!obj) return null;

        StrBuf buf();
        if (obj as Map) {
            str[] pairs();
            for (key, value in obj) {
                str keyKey = cacheKeyArg(key, depth+1);
                str valueKey = cacheKeyArg(value, depth+1);
                pairs << keyKey # valueKey;
            }
            for (pair in pairs.sorted) buf << pair;
        } else if (obj as Seq) {
            for (value in obj) {
                str valueKey = cacheKeyArg(value, depth+1);
                buf << valueKey;
            }
        } else if (obj as Set) {
            str[] keys();
            for (value in obj) {
                str valueKey = cacheKeyArg(value, depth+1);
                keys << valueKey;
            }
            for (key in keys.sorted) buf << key;
        } else if (obj as Material) {
            buf << cacheKeyArg(obj.pkg, depth+1);
            buf << cacheKeyArg(obj.id, depth+1);
        } else if (obj as UnitLoad) {
            buf << obj.uniqueKey();
        } else {
            buf << obj;
        }
        str text = buf.toS();
        return obj.class.id.hex # ':' # text.count # ':' # text;
    }
}

cm.geometry2D.advanced

Polyline inside tests and polygon difference

Polyline inside checks using APolyline2D.insideCount() now return the correct result in cases where they previously misclassified a point near a closing vertex. This can change behavior in extension code that relies on those checks.

The difference() shape Boolean operation also handles an acute-corner case where it previously could return the original outline without removing the cutter. Review code and tests that work around either issue.

cm.test

test.syntax.cm

The minitest syntax now restore the old value of the testSuiteLog.verbose flag once it is finished. This fixes an issue where nested calls could unintentionally modify the verbose state of all subsequent tests.

minitest(true) {
    test 1 + 1 => 2; // Printed to log
    minitest(false) {
	test 2 + 2 => 4; // Not printed to log
    }
    test 5 + 5 => 10; // Previously not printed to log. Now printed to log.
}

Please be aware that it is still adviced to avoid nested test suite logging, since all features may not be fully supported.

cm.test.cmunit

basicTestCase.cm

The testCases function now restores the old values of the testSuiteLog.verbose and testSuiteLog.hideErrors flags once it is finished. This fixes an issue where nested calls could unintentionally modify the verbose and hideErrors state of all subsequent tests.

Please be aware that it is still adviced to avoid nested test suite logging, since all features may not be fully supported.