From 6810c7d5fa499495d5a2e8fb6008396caf235cc1 Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:49:31 +0200 Subject: [PATCH 1/8] Merge support-vertex groups from different origins into one section Support vertices can be classified two ways: moveSupportVertices pulls out highly-connected nodes via the threshold, and addVertexTreeToForest separately classifies letter-ending groups as SupportTree directly. Both used to key their OMap1 entry differently (SupportKey vs a prefix-derived key), so they never collided and ended up as two separate "Support nodes" sections with duplicated/misplaced metadata instead of one merged section. Also fixes two spots where a collision in an OMap1 merge (in mergeOMap1Trees and the near-identical insertTreeInMap) discarded the incoming side's comments unconditionally, even when the existing side had none to fall back on. Regenerated examples/transformed_jbeam/fender-cfg-{default,example}.jbeam via jbeam-edit-dump-ast; frame and suspension are unaffected. --- .../transformed_jbeam/fender-cfg-default.jbeam | 6 ++---- .../transformed_jbeam/fender-cfg-example.jbeam | 6 ++---- .../transformation/JbeamEdit/Transformation.hs | 18 ++++++++++++++++-- .../Transformation/VertexExtraction.hs | 3 ++- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/examples/transformed_jbeam/fender-cfg-default.jbeam b/examples/transformed_jbeam/fender-cfg-default.jbeam index 57e71067..6f0b6b7e 100644 --- a/examples/transformed_jbeam/fender-cfg-default.jbeam +++ b/examples/transformed_jbeam/fender-cfg-default.jbeam @@ -49,15 +49,13 @@ {"selfCollision" : false}, ["bfsl", 0.684, -1.079, 0.507], ["bfsr", -0.623, -1.064, 0.507], - - // Support nodes - {"group" : "cot_fender_r"}, - ["bfr_sr", -0.906, -1.737, 0.578], {"collision" : true}, {"group" : "cot_fender_l"}, {"nodeWeight" : 0.65}, {"selfCollision" : true}, ["bfl_sl", 0.959, -1.762, 0.576], + {"group" : "cot_fender_r"}, + ["bfr_sr", -0.906, -1.737, 0.578], ], "beams" : [ ["id1:", "id2:"], diff --git a/examples/transformed_jbeam/fender-cfg-example.jbeam b/examples/transformed_jbeam/fender-cfg-example.jbeam index 57e71067..6f0b6b7e 100644 --- a/examples/transformed_jbeam/fender-cfg-example.jbeam +++ b/examples/transformed_jbeam/fender-cfg-example.jbeam @@ -49,15 +49,13 @@ {"selfCollision" : false}, ["bfsl", 0.684, -1.079, 0.507], ["bfsr", -0.623, -1.064, 0.507], - - // Support nodes - {"group" : "cot_fender_r"}, - ["bfr_sr", -0.906, -1.737, 0.578], {"collision" : true}, {"group" : "cot_fender_l"}, {"nodeWeight" : 0.65}, {"selfCollision" : true}, ["bfl_sl", 0.959, -1.762, 0.576], + {"group" : "cot_fender_r"}, + ["bfr_sr", -0.906, -1.737, 0.578], ], "beams" : [ ["id1:", "id2:"], diff --git a/src-extra/transformation/JbeamEdit/Transformation.hs b/src-extra/transformation/JbeamEdit/Transformation.hs index 34681f0e..234aa5bf 100644 --- a/src-extra/transformation/JbeamEdit/Transformation.hs +++ b/src-extra/transformation/JbeamEdit/Transformation.hs @@ -110,6 +110,19 @@ groupByPrefix origTree = . NE.map (prefixForVertexKey origTree) . NE.groupWith1 (dropIndex . vName . aVertex) +{- | Like 'groupByPrefix', but support vertices always share a single +SupportKey bucket instead of being split by name prefix. +-} +groupForType + :: VertexTreeType + -> Maybe (OMap1 VertexTreeKey VertexTree) + -> NonEmpty AnnotatedVertex + -> OMap1 VertexTreeKey VertexTree +groupForType SupportTree origTree vs = + let topComments = concatMap tComments (OMap1.lookup SupportKey =<< origTree) + in OMap1.singleton (SupportKey, VertexTree topComments vs) +groupForType _ origTree vs = groupByPrefix origTree vs + commentsExists :: Maybe (OMap1 VertexTreeKey VertexTree) -> Bool commentsExists = any (notNull . tComments . OMap1.head) @@ -129,7 +142,7 @@ addVertexTreeToForest newNames tf grouped forest forestAcc t = addSideComment t (commentsExists origTree) . addPrefixComments t . fmap (sortVertices t newNames tf) - $ groupByPrefix origTree groupsForT + $ groupForType t origTree groupsForT in Right (M.insertWith mergeOMap1Trees t tree forestAcc) Nothing -> Right forestAcc @@ -140,7 +153,8 @@ mergeOMap1Trees mergeOMap1Trees new existing = foldl' go existing (OMap1.assocs new) where - merge (VertexTree _ newVerts) (VertexTree ec ev) = VertexTree ec (ev <> newVerts) + merge (VertexTree nc newVerts) (VertexTree ec ev) = + VertexTree (bool nc ec (notNull ec)) (ev <> newVerts) go acc (k, v) = OMap1.insertWith merge k v acc groupAnnotatedVertices diff --git a/src-extra/transformation/JbeamEdit/Transformation/VertexExtraction.hs b/src-extra/transformation/JbeamEdit/Transformation/VertexExtraction.hs index 38ae1dae..2cfb73ce 100644 --- a/src-extra/transformation/JbeamEdit/Transformation/VertexExtraction.hs +++ b/src-extra/transformation/JbeamEdit/Transformation/VertexExtraction.hs @@ -148,7 +148,8 @@ insertTreeInMap :: VertexTree -> OMap1 VertexTreeKey VertexTree -> OMap1 VertexTreeKey VertexTree insertTreeInMap (VertexTree newComments newVertexGroups) omap = let vType = getVertexTreePrefix newVertexGroups - merge _ (VertexTree ec ev) = VertexTree ec (ev <> newVertexGroups) + merge (VertexTree nc _) (VertexTree ec ev) = + VertexTree (if null ec then nc else ec) (ev <> newVertexGroups) in OMap1.insertWith merge vType (VertexTree newComments newVertexGroups) omap isSupportVertex :: Vertex -> Bool From 3f8c2c306c1f75e01dbf4ec028f6e1b7c1f7b0c5 Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:54:55 +0200 Subject: [PATCH 2/8] Derive support vertex names without pre-mutating them --transform was not a fixed point. updateSupportVertexName appended a side letter to the name before assignNames ran, and assignNames was written to recognise its own output one character further along. Running the tool twice therefore grew the name: nlsl1 became nlslsl. Drop the pre-mutation and let assignNames derive the whole name from the source name and the x coordinate. A support prefix is now the name with its trailing digits, side letter and s removed, followed by s and the side letter for the group the coordinate falls in. That is a fixed point, so a second pass lands on the same name. Frame's rear support node changes from rl_rsm to rl_sm. The side letter comes from the coordinate rather than from the name, and _r is a side letter as far as the transformation can tell, so it is dropped. The ordinary nodes in that group already lost it: rl_r35 and rl_r38 have been coming out as rl_m0 and rl_m1 all along. The two now agree. The test transforms a fixture with three support hubs sharing one prefix group twice and asserts the second pass renames nothing further. The fixture lives in examples/regression_jbeam/ so it stays out of jbeam-edit-dump-ast's scan and out of the curated example set. --- examples/regression_jbeam/README.md | 9 +++ .../support-rename-idempotency-repro.jbeam | 34 ++++++++++ .../fender-cfg-default.jbeam | 50 +++++++-------- .../fender-cfg-example.jbeam | 50 +++++++-------- .../transformed_jbeam/frame-cfg-default.jbeam | 34 +++++----- .../transformed_jbeam/frame-cfg-example.jbeam | 34 +++++----- .../JbeamEdit/Transformation.hs | 64 +++++++++---------- test-extra/transformation/Spec.hs | 60 ++++++++++++++++- 8 files changed, 218 insertions(+), 117 deletions(-) create mode 100644 examples/regression_jbeam/README.md create mode 100644 examples/regression_jbeam/support-rename-idempotency-repro.jbeam diff --git a/examples/regression_jbeam/README.md b/examples/regression_jbeam/README.md new file mode 100644 index 00000000..ed93e21f --- /dev/null +++ b/examples/regression_jbeam/README.md @@ -0,0 +1,9 @@ +# Regression jbeam fixtures + +Small `.jbeam` files that exist purely to reproduce a specific bug for a +regression test. Unlike `examples/jbeam/`, these are **not** written or +vetted by the jbeam maintainer, not curated demo material, and +not picked up by `jbeam-edit-dump-ast` (which only scans `examples/jbeam/`). +Don't treat them as examples of good jbeam, and don't add to this +directory unless a test genuinely needs a fixture that can't be built +from what's already in the project. diff --git a/examples/regression_jbeam/support-rename-idempotency-repro.jbeam b/examples/regression_jbeam/support-rename-idempotency-repro.jbeam new file mode 100644 index 00000000..b258fe3d --- /dev/null +++ b/examples/regression_jbeam/support-rename-idempotency-repro.jbeam @@ -0,0 +1,34 @@ +{ +"testpart":{ + "nodes":[ + ["id", "posX", "posY", "posZ"], + ["nl0", 1.0, -3.0, 0.0], + ["nl1", 1.0, -3.3, 0.0], + ["nl2", 1.0, -3.6, 0.0], + ["nl3", 1.0, -3.9, 0.5], + ["nl10", 1.0, 0.0, 0.0], + ["nl11", 1.0, 0.3, 0.0], + ["nl12", 1.0, 0.6, 0.0], + ["nl13", 1.0, 0.9, 0.5], + ["nl20", 1.0, 3.0, 0.0], + ["nl21", 1.0, 3.3, 0.0], + ["nl22", 1.0, 3.6, 0.0], + ["nl23", 1.0, 3.9, 0.5], + ], + "beams":[ + ["id1:", "id2:"], + ["nl0", "nl1"], + ["nl0", "nl2"], + ["nl0", "nl3"], + ["nl10", "nl11"], + ["nl10", "nl12"], + ["nl10", "nl13"], + ["nl20", "nl21"], + ["nl20", "nl22"], + ["nl20", "nl23"], + ], + "triangles":[ + ["id1:", "id2:", "id3:"], + ], +}, +} diff --git a/examples/transformed_jbeam/fender-cfg-default.jbeam b/examples/transformed_jbeam/fender-cfg-default.jbeam index 6f0b6b7e..ddc92ab3 100644 --- a/examples/transformed_jbeam/fender-cfg-default.jbeam +++ b/examples/transformed_jbeam/fender-cfg-default.jbeam @@ -14,48 +14,48 @@ }, "slotType" : "cot_fender", "nodes" : [ - ["id", "posX", "posY", "posZ"], + ["id", "posX", "posY", "posZ"], {"frictionCoef" : 0.7}, {"nodeMaterial" : "|NM_METAL"}, {"collision" : true}, {"group" : "cot_fender_l"}, {"nodeWeight" : 0.65}, {"selfCollision" : true}, - ["bfl0", 0.739, -1.845, 0.716], - ["bfl1", 0.855, -1.788, 0.707], - ["bfl2", 0.948, -1.435, 0.730], - ["bfl3", 0.756, -1.413, 0.843], - ["bfl4", 0.963, -1.024, 0.112], - ["bfl5", 0.964, -1.072, 0.507], - ["bfl6", 0.778, -1.008, 0.873], - ["bfl7", 0.987, -0.743, 0.109], - ["bfl8", 0.987, -0.744, 0.494], - ["bfl9", 0.812, -0.759, 0.896], - ["bfr0", -0.691, -1.829, 0.716], - ["bfr1", -0.807, -1.769, 0.707], - ["bfr2", -0.890, -1.409, 0.729], - ["bfr3", -0.700, -1.397, 0.843], - ["bfr4", -0.899, -1.005, 0.112], - ["bfr5", -0.900, -1.053, 0.508], - ["bfr6", -0.715, -0.991, 0.873], - ["bfr7", -0.916, -0.742, 0.112], - ["bfr8", -0.917, -0.746, 0.494], - ["bfr9", -0.734, -0.746, 0.888], + ["bfl0", 0.739, -1.845, 0.716], + ["bfl1", 0.855, -1.788, 0.707], + ["bfl2", 0.948, -1.435, 0.730], + ["bfl3", 0.756, -1.413, 0.843], + ["bfl4", 0.963, -1.024, 0.112], + ["bfl5", 0.964, -1.072, 0.507], + ["bfl6", 0.778, -1.008, 0.873], + ["bfl7", 0.987, -0.743, 0.109], + ["bfl8", 0.987, -0.744, 0.494], + ["bfl9", 0.812, -0.759, 0.896], + ["bfr0", -0.691, -1.829, 0.716], + ["bfr1", -0.807, -1.769, 0.707], + ["bfr2", -0.890, -1.409, 0.729], + ["bfr3", -0.700, -1.397, 0.843], + ["bfr4", -0.899, -1.005, 0.112], + ["bfr5", -0.900, -1.053, 0.508], + ["bfr6", -0.715, -0.991, 0.873], + ["bfr7", -0.916, -0.742, 0.112], + ["bfr8", -0.917, -0.746, 0.494], + ["bfr9", -0.734, -0.746, 0.888], // Support nodes {"collision" : false}, {"group" : ""}, {"nodeWeight" : 1.2}, {"selfCollision" : false}, - ["bfsl", 0.684, -1.079, 0.507], - ["bfsr", -0.623, -1.064, 0.507], + ["bfsl", 0.684, -1.079, 0.507], + ["bfsr", -0.623, -1.064, 0.507], {"collision" : true}, {"group" : "cot_fender_l"}, {"nodeWeight" : 0.65}, {"selfCollision" : true}, - ["bfl_sl", 0.959, -1.762, 0.576], + ["bfl_fsl", 0.959, -1.762, 0.576], {"group" : "cot_fender_r"}, - ["bfr_sr", -0.906, -1.737, 0.578], + ["bfr_fsr", -0.906, -1.737, 0.578], ], "beams" : [ ["id1:", "id2:"], diff --git a/examples/transformed_jbeam/fender-cfg-example.jbeam b/examples/transformed_jbeam/fender-cfg-example.jbeam index 6f0b6b7e..ddc92ab3 100644 --- a/examples/transformed_jbeam/fender-cfg-example.jbeam +++ b/examples/transformed_jbeam/fender-cfg-example.jbeam @@ -14,48 +14,48 @@ }, "slotType" : "cot_fender", "nodes" : [ - ["id", "posX", "posY", "posZ"], + ["id", "posX", "posY", "posZ"], {"frictionCoef" : 0.7}, {"nodeMaterial" : "|NM_METAL"}, {"collision" : true}, {"group" : "cot_fender_l"}, {"nodeWeight" : 0.65}, {"selfCollision" : true}, - ["bfl0", 0.739, -1.845, 0.716], - ["bfl1", 0.855, -1.788, 0.707], - ["bfl2", 0.948, -1.435, 0.730], - ["bfl3", 0.756, -1.413, 0.843], - ["bfl4", 0.963, -1.024, 0.112], - ["bfl5", 0.964, -1.072, 0.507], - ["bfl6", 0.778, -1.008, 0.873], - ["bfl7", 0.987, -0.743, 0.109], - ["bfl8", 0.987, -0.744, 0.494], - ["bfl9", 0.812, -0.759, 0.896], - ["bfr0", -0.691, -1.829, 0.716], - ["bfr1", -0.807, -1.769, 0.707], - ["bfr2", -0.890, -1.409, 0.729], - ["bfr3", -0.700, -1.397, 0.843], - ["bfr4", -0.899, -1.005, 0.112], - ["bfr5", -0.900, -1.053, 0.508], - ["bfr6", -0.715, -0.991, 0.873], - ["bfr7", -0.916, -0.742, 0.112], - ["bfr8", -0.917, -0.746, 0.494], - ["bfr9", -0.734, -0.746, 0.888], + ["bfl0", 0.739, -1.845, 0.716], + ["bfl1", 0.855, -1.788, 0.707], + ["bfl2", 0.948, -1.435, 0.730], + ["bfl3", 0.756, -1.413, 0.843], + ["bfl4", 0.963, -1.024, 0.112], + ["bfl5", 0.964, -1.072, 0.507], + ["bfl6", 0.778, -1.008, 0.873], + ["bfl7", 0.987, -0.743, 0.109], + ["bfl8", 0.987, -0.744, 0.494], + ["bfl9", 0.812, -0.759, 0.896], + ["bfr0", -0.691, -1.829, 0.716], + ["bfr1", -0.807, -1.769, 0.707], + ["bfr2", -0.890, -1.409, 0.729], + ["bfr3", -0.700, -1.397, 0.843], + ["bfr4", -0.899, -1.005, 0.112], + ["bfr5", -0.900, -1.053, 0.508], + ["bfr6", -0.715, -0.991, 0.873], + ["bfr7", -0.916, -0.742, 0.112], + ["bfr8", -0.917, -0.746, 0.494], + ["bfr9", -0.734, -0.746, 0.888], // Support nodes {"collision" : false}, {"group" : ""}, {"nodeWeight" : 1.2}, {"selfCollision" : false}, - ["bfsl", 0.684, -1.079, 0.507], - ["bfsr", -0.623, -1.064, 0.507], + ["bfsl", 0.684, -1.079, 0.507], + ["bfsr", -0.623, -1.064, 0.507], {"collision" : true}, {"group" : "cot_fender_l"}, {"nodeWeight" : 0.65}, {"selfCollision" : true}, - ["bfl_sl", 0.959, -1.762, 0.576], + ["bfl_fsl", 0.959, -1.762, 0.576], {"group" : "cot_fender_r"}, - ["bfr_sr", -0.906, -1.737, 0.578], + ["bfr_fsr", -0.906, -1.737, 0.578], ], "beams" : [ ["id1:", "id2:"], diff --git a/examples/transformed_jbeam/frame-cfg-default.jbeam b/examples/transformed_jbeam/frame-cfg-default.jbeam index ebee3b4b..1ce19b0e 100644 --- a/examples/transformed_jbeam/frame-cfg-default.jbeam +++ b/examples/transformed_jbeam/frame-cfg-default.jbeam @@ -92,9 +92,9 @@ ["rlr7", -0.715, 0.791, 0.233], // Support nodes - ["rl_fsm", 0.053, -1.314, 0.382], // support for front - ["rl_rsm", 0.053, 1.710, 0.565], // support for rear ["rlsm", 0.053, -0.024, 0.578], // support + ["rl_fsm", 0.053, -1.314, 0.382], // support for front + ["rl_sm", 0.053, 1.710, 0.565], // support for rear ], // --Beams-- "beams" : [ @@ -320,21 +320,21 @@ // Rear end {"beamDeform" : 19000}, - ["rl_rsm", "rl_m2"], - ["rl_rsm", "rl_r7"], - ["rl_r4", "rl_rsm"], - ["rl_rsm", "rl_l7"], - ["rl_rsm", "rl_l3"], - ["rl_rsm", "rl_r5"], - ["rl_l6", "rl_rsm"], - ["rl_m1", "rl_rsm"], - ["rl_rsm", "rl_r3"], - ["rl_l4", "rl_rsm"], - ["rl_r6", "rl_rsm"], - ["rl_m3", "rl_rsm"], - ["rl_l2", "rl_rsm"], - ["rl_rsm", "rl_m0"], - ["rl_r2", "rl_rsm"], + ["rl_sm", "rl_m2"], + ["rl_sm", "rl_r7"], + ["rl_r4", "rl_sm"], + ["rl_sm", "rl_l7"], + ["rl_sm", "rl_l3"], + ["rl_sm", "rl_r5"], + ["rl_l6", "rl_sm"], + ["rl_m1", "rl_sm"], + ["rl_sm", "rl_r3"], + ["rl_l4", "rl_sm"], + ["rl_r6", "rl_sm"], + ["rl_m3", "rl_sm"], + ["rl_l2", "rl_sm"], + ["rl_sm", "rl_m0"], + ["rl_r2", "rl_sm"], ["rl_r4", "rl_l5"], // Front crush diff --git a/examples/transformed_jbeam/frame-cfg-example.jbeam b/examples/transformed_jbeam/frame-cfg-example.jbeam index ebee3b4b..1ce19b0e 100644 --- a/examples/transformed_jbeam/frame-cfg-example.jbeam +++ b/examples/transformed_jbeam/frame-cfg-example.jbeam @@ -92,9 +92,9 @@ ["rlr7", -0.715, 0.791, 0.233], // Support nodes - ["rl_fsm", 0.053, -1.314, 0.382], // support for front - ["rl_rsm", 0.053, 1.710, 0.565], // support for rear ["rlsm", 0.053, -0.024, 0.578], // support + ["rl_fsm", 0.053, -1.314, 0.382], // support for front + ["rl_sm", 0.053, 1.710, 0.565], // support for rear ], // --Beams-- "beams" : [ @@ -320,21 +320,21 @@ // Rear end {"beamDeform" : 19000}, - ["rl_rsm", "rl_m2"], - ["rl_rsm", "rl_r7"], - ["rl_r4", "rl_rsm"], - ["rl_rsm", "rl_l7"], - ["rl_rsm", "rl_l3"], - ["rl_rsm", "rl_r5"], - ["rl_l6", "rl_rsm"], - ["rl_m1", "rl_rsm"], - ["rl_rsm", "rl_r3"], - ["rl_l4", "rl_rsm"], - ["rl_r6", "rl_rsm"], - ["rl_m3", "rl_rsm"], - ["rl_l2", "rl_rsm"], - ["rl_rsm", "rl_m0"], - ["rl_r2", "rl_rsm"], + ["rl_sm", "rl_m2"], + ["rl_sm", "rl_r7"], + ["rl_r4", "rl_sm"], + ["rl_sm", "rl_l7"], + ["rl_sm", "rl_l3"], + ["rl_sm", "rl_r5"], + ["rl_l6", "rl_sm"], + ["rl_m1", "rl_sm"], + ["rl_sm", "rl_r3"], + ["rl_l4", "rl_sm"], + ["rl_r6", "rl_sm"], + ["rl_m3", "rl_sm"], + ["rl_l2", "rl_sm"], + ["rl_sm", "rl_m0"], + ["rl_r2", "rl_sm"], ["rl_r4", "rl_l5"], // Front crush diff --git a/src-extra/transformation/JbeamEdit/Transformation.hs b/src-extra/transformation/JbeamEdit/Transformation.hs index 234aa5bf..4558a856 100644 --- a/src-extra/transformation/JbeamEdit/Transformation.hs +++ b/src-extra/transformation/JbeamEdit/Transformation.hs @@ -163,15 +163,6 @@ groupAnnotatedVertices -> Either Text (VertexTreeType, [AnnotatedVertex]) groupAnnotatedVertices brks g = (,[g]) <$> determineGroup' brks (aVertex g) -updateSupportVertexName - :: VertexTreeType - -> AnnotatedVertex - -> AnnotatedVertex -updateSupportVertexName vType (AnnotatedVertex c v m) = AnnotatedVertex c (v {vName = newName}) m - where - name = vName v - newName = dropIndex name <> prefixForType vType - moveSupportVertices :: UpdateNamesMap -> TransformationConfig @@ -179,10 +170,10 @@ moveSupportVertices -> M.Map VertexTreeType [AnnotatedVertex] -> (VertexForest, M.Map VertexTreeType [AnnotatedVertex]) moveSupportVertices newNames tfCfg connMap vsPerType = - let supportVertices :: [(VertexTreeType, AnnotatedVertex)] + let supportVertices :: [AnnotatedVertex] supportVertices = - [ (vType, av) - | (vType, vs) <- M.toList vsPerType + [ av + | vs <- M.elems vsPerType , av <- vs , let name = vName (aVertex av) , let vertexCount = length vs @@ -209,16 +200,13 @@ moveSupportVertices newNames tfCfg connMap vsPerType = , VertexTree [sideComment SupportTree] ( snd - . mapAccumL - assignSupportNames - M.empty - . NE.sortBy (compareAV thr SupportTree) - $ NE.map (uncurry updateSupportVertexName) vs + . mapAccumL assignSupportNames M.empty + $ NE.sortBy (compareAV thr SupportTree) vs ) ) ) - supportVertexNames = foldr (S.insert . anVertexName . snd) S.empty supportVertices + supportVertexNames = foldr (S.insert . anVertexName) S.empty supportVertices remainingVertices :: M.Map VertexTreeType [AnnotatedVertex] remainingVertices = @@ -372,6 +360,26 @@ renameVertexId treeType idx vertexPrefix = let idx' = mwhen (treeType /= SupportTree || idx /= 0) (intToText idx) in vertexPrefix <> idx' +sideLetters :: String +sideLetters = ['l', 'm', 'r'] + +{- | Strip a trailing side letter and a trailing @s@ from a prefix. Feeding an +already renamed support vertex back in then lands on the same name: @rl_fsm@ +strips to @rl_f@ and is built back up to @rl_fsm@. +-} +dropSupportSuffix :: Text -> Text +dropSupportSuffix prefix = + bool + withoutSide + (T.init withoutSide) + (T.length withoutSide >= 2 && T.last withoutSide == 's') + where + withoutSide = + bool + prefix + (T.init prefix) + (T.length prefix > 2 && T.last prefix `elem` sideLetters) + assignNames :: UpdateNamesMap -> XGroupBreakpoints @@ -385,26 +393,18 @@ assignNames newNames brks treeType prefixMap av = prefix = dropIndex (vName v) typeSpecific = either (const "") prefixForType (determineGroup brks v) (prefix', lastChar) = fromMaybe (error "unreachable") (T.unsnoc prefix) - isLmr = lastChar `elem` ['l', 'm', 'r'] - supportPrefixChar = T.singleton 's' <> bool typeSpecific (T.singleton lastChar) isLmr + isLmr = lastChar `elem` sideLetters cleanPrefix - | treeType /= SupportTree - && T.length prefix >= 3 + | treeType == SupportTree = + updatedPrefix (dropSupportSuffix prefix) <> T.singleton 's' <> typeSpecific + | T.length prefix >= 3 && T.last prefix' == 's' = updatedPrefix (T.init prefix') <> typeSpecific - | treeType /= SupportTree - && T.length prefix >= 3 + | T.length prefix >= 3 && isLmr = updatedPrefix prefix' <> typeSpecific - | treeType /= SupportTree = - updatedPrefix prefix <> typeSpecific - | T.length prefix' >= 3 - && T.last prefix' == 's' = - updatedPrefix (T.init prefix') <> T.singleton 's' <> typeSpecific - | T.length prefix' < 2 = - updatedPrefix prefix <> supportPrefixChar | otherwise = - updatedPrefix prefix' <> supportPrefixChar + updatedPrefix prefix <> typeSpecific lastIdx = M.findWithDefault 0 cleanPrefix prefixMap newName = renameVertexId treeType lastIdx cleanPrefix newVertex = v {vName = newName} diff --git a/test-extra/transformation/Spec.hs b/test-extra/transformation/Spec.hs index c3463f55..e5a0cfe5 100644 --- a/test-extra/transformation/Spec.hs +++ b/test-extra/transformation/Spec.hs @@ -5,8 +5,12 @@ module Spec ( import Data.List (isPrefixOf, isSuffixOf) import Data.Map qualified as M import Data.Set qualified as S +import Data.Text (Text) import Data.Text qualified as T -import JbeamEdit.Core.Node (Node) +import Data.Vector qualified as V +import GHC.IsList (fromList) +import JbeamEdit.Core.Node (Node (..), NumberValue (..), expectArray) +import JbeamEdit.Core.NodePath qualified as NP import JbeamEdit.Formatting import JbeamEdit.IOUtils (tryReadFile) import JbeamEdit.Parsing.Jbeam (parseNodes) @@ -85,6 +89,59 @@ beamValidationSpec = do it "has no duplicate beams" $ findDuplicateBeams internalBeams `shouldBe` [] +nodesQuery :: NP.NodePath +nodesQuery = fromList [NP.ObjectIndex 0, NP.ObjectKey "nodes"] + +{- | (name, Y position) for every vertex in a top node's "nodes" section, +in file order. +-} +vertexPositionsInOrder :: Node -> [(Text, Double)] +vertexPositionsInOrder topNode = + case NP.queryNodes nodesQuery topNode >>= NP.expectArray nodesQuery of + Left _ -> [] + Right rows -> + [ (name, realToFrac (nvValue yNum)) + | row <- V.toList rows + , Just inner <- [expectArray row] + , Just (String name) <- [inner V.!? 0] + , name /= "id" + , Just (Number yNum) <- [inner V.!? 2] + ] + +{- | Three small hubs (nl0, nl10, nl20; front/mid/rear), each beamed to +three of its own ordinary leaf nodes (see issue #215). At +support-threshold 20 with 12 nodes in the group, thrCount = +round(0.2*12) = 2: each hub (3 connections) clears it and becomes a +support vertex, each leaf (1 connection) doesn't. This mirrors the +support-hub shape seen in real body files (three support nodes sharing +one prefix group) at a size small enough to reason about by hand: after +the first transform, the mid/rear hubs get a trailing index (e.g. nlsl1, +nlsl2) while the front one doesn't (nlsl), and that index is exactly +what trips up the second pass. Y positions are spaced well outside the +(default 0.05) y-sorting-threshold so this test stays isolated from the +separate y-sorting-threshold banding bug (issue #214). +-} +supportRenameIdempotencyFixture :: FilePath +supportRenameIdempotencyFixture = + "examples/regression_jbeam/support-rename-idempotency-repro.jbeam" + +supportRenameIdempotencySpec :: Spec +supportRenameIdempotencySpec = + describe "support vertex renaming" + . it "is a fixed point: transforming the output again renames nothing further" + $ do + let cfg = newTransformationConfig {supportThreshold = 20} + topNode <- parseJbeamFile supportRenameIdempotencyFixture + case transform M.empty cfg topNode of + Left err -> expectationFailure ("first transform failed: " ++ T.unpack err) + Right (_, _, _, onceNode) -> + case transform M.empty cfg onceNode of + Left err -> expectationFailure ("second transform failed: " ++ T.unpack err) + Right (_, _, _, twiceNode) -> do + let once = vertexPositionsInOrder onceNode + once `shouldNotBe` [] + vertexPositionsInOrder twiceNode `shouldBe` once + main :: IO () main = hspec $ do let exampleConfigPath = unsafeEncodeUtf "examples/jbeam-edit.yaml" @@ -102,3 +159,4 @@ main = hspec $ do mapM_ (testInputFile "cfg-default" newTransformationConfig) inputFiles mapM_ (testInputFile "cfg-example" tfConfig) inputFiles beamValidationSpec + supportRenameIdempotencySpec From 872c11afeee70918e0563991866004f171995d19 Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:55:52 +0200 Subject: [PATCH 3/8] Rename bfl_f and bfr_f back to bfl0 and bfr0 They were renamed to prove that letter-ending names survive a transform, but only the two node rows were changed. Every beam and triangle kept referring to bfl0 and bfr0, so the example has been carrying two dangling references ever since. The missing digit suffix also broke prefix-key lookup, which is what silently dropped the Left side and Right side comments from the output. The property they were there to prove now has its own fixture in examples/regression_jbeam/, with a test that asserts every vertex coordinate survives a transform. That states it directly instead of leaving it implicit in a node count, and it keeps scaffolding out of an example the jbeam maintainer edits. Fender's output goes back to the pre-regression structure: side comment, side metadata, side coordinates, then one support section. --- examples/ast/jbeam/fender.hs | 4 +- .../formatted_jbeam/fender-complex-jbfl.jbeam | 4 +- .../formatted_jbeam/fender-minimal-jbfl.jbeam | 4 +- examples/jbeam/fender.jbeam | 4 +- .../letter-ending-nodes-repro.jbeam | 34 ++ .../fender-after-frame-cfg-default.jbeam | 4 +- .../fender-after-frame-cfg-example.jbeam | 4 +- .../fender-cfg-default.jbeam | 366 +++++++++--------- .../fender-cfg-example.jbeam | 366 +++++++++--------- test-extra/transformation/Spec.hs | 42 ++ 10 files changed, 454 insertions(+), 378 deletions(-) create mode 100644 examples/regression_jbeam/letter-ending-nodes-repro.jbeam diff --git a/examples/ast/jbeam/fender.hs b/examples/ast/jbeam/fender.hs index e3d1fe49..3d6d6dd5 100644 --- a/examples/ast/jbeam/fender.hs +++ b/examples/ast/jbeam/fender.hs @@ -94,7 +94,7 @@ Object ( ArrayValue { avElements = [ - ( String "bfl_f", True ), + ( String "bfl0", True ), ( Number ( NumberValue { nvText = "0.959", nvValue = 0.959 } ), True ), @@ -257,7 +257,7 @@ Object ( ArrayValue { avElements = [ - ( String "bfr_f", True ), + ( String "bfr0", True ), ( Number ( NumberValue { nvText = "-0.906", nvValue = -0.906 } ), True ), diff --git a/examples/formatted_jbeam/fender-complex-jbfl.jbeam b/examples/formatted_jbeam/fender-complex-jbfl.jbeam index ff26f356..76ed8b98 100644 --- a/examples/formatted_jbeam/fender-complex-jbfl.jbeam +++ b/examples/formatted_jbeam/fender-complex-jbfl.jbeam @@ -23,7 +23,7 @@ // Left side {"group" : "cot_fender_l"}, - ["bfl_f", 0.959, -1.762, 0.576], + ["bfl0", 0.959, -1.762, 0.576], ["bfl1", 0.855, -1.788, 0.707], ["bfl2", 0.739, -1.845, 0.716], ["bfl3", 0.948, -1.435, 0.730], @@ -37,7 +37,7 @@ // Right side {"group" : "cot_fender_r"}, - ["bfr_f", -0.906, -1.737, 0.578], + ["bfr0", -0.906, -1.737, 0.578], ["bfr1", -0.807, -1.769, 0.707], ["bfr2", -0.691, -1.829, 0.716], ["bfr3", -0.890, -1.409, 0.729], diff --git a/examples/formatted_jbeam/fender-minimal-jbfl.jbeam b/examples/formatted_jbeam/fender-minimal-jbfl.jbeam index 480487b8..95ffe41f 100644 --- a/examples/formatted_jbeam/fender-minimal-jbfl.jbeam +++ b/examples/formatted_jbeam/fender-minimal-jbfl.jbeam @@ -23,7 +23,7 @@ // Left side {"group" : "cot_fender_l"}, - ["bfl_f", 0.959, -1.762, 0.576], + ["bfl0", 0.959, -1.762, 0.576], ["bfl1", 0.855, -1.788, 0.707], ["bfl2", 0.739, -1.845, 0.716], ["bfl3", 0.948, -1.435, 0.730], @@ -37,7 +37,7 @@ // Right side {"group" : "cot_fender_r"}, - ["bfr_f", -0.906, -1.737, 0.578], + ["bfr0", -0.906, -1.737, 0.578], ["bfr1", -0.807, -1.769, 0.707], ["bfr2", -0.691, -1.829, 0.716], ["bfr3", -0.890, -1.409, 0.729], diff --git a/examples/jbeam/fender.jbeam b/examples/jbeam/fender.jbeam index 10c4efca..43a08944 100644 --- a/examples/jbeam/fender.jbeam +++ b/examples/jbeam/fender.jbeam @@ -25,7 +25,7 @@ //Left side {"group":"cot_fender_l"}, - ["bfl_f",0.959,-1.762,0.576], + ["bfl0",0.959,-1.762,0.576], ["bfl1",0.855,-1.788,0.707], ["bfl2",0.739,-1.845,0.716], ["bfl3",0.948,-1.435,0.730], @@ -39,7 +39,7 @@ //Right side {"group":"cot_fender_r"}, - ["bfr_f",-0.906,-1.737,0.578], + ["bfr0",-0.906,-1.737,0.578], ["bfr1",-0.807,-1.769,0.707], ["bfr2",-0.691,-1.829,0.716], ["bfr3",-0.890,-1.409,0.729], diff --git a/examples/regression_jbeam/letter-ending-nodes-repro.jbeam b/examples/regression_jbeam/letter-ending-nodes-repro.jbeam new file mode 100644 index 00000000..7188fd2a --- /dev/null +++ b/examples/regression_jbeam/letter-ending-nodes-repro.jbeam @@ -0,0 +1,34 @@ +{ +"testpart":{ + "nodes":[ + ["id", "posX", "posY", "posZ"], + // Names ending in a letter rather than a digit, in several prefix + // groups. These used to be dropped silently: every such group got + // the same SupportKey, and the insert replaced instead of merged, + // so only the last group survived. + ["nl_f", 1.0, -1.0, 0.0], + ["nr_f", -1.0, -1.0, 0.0], + ["nll", 1.0, 0.0, 0.0], + ["nrr", -1.0, 0.0, 0.0], + ["nl_r", 1.0, 1.0, 0.0], + ["nr_r", -1.0, 1.0, 0.0], + // Digit-ending names alongside them, so both paths are exercised. + ["nl0", 1.0, 2.0, 0.0], + ["nr0", -1.0, 2.0, 0.0], + ], + "beams":[ + ["id1:", "id2:"], + ["nl_f", "nll"], + ["nll", "nl_r"], + ["nl_r", "nl0"], + ["nr_f", "nrr"], + ["nrr", "nr_r"], + ["nr_r", "nr0"], + ], + "triangles":[ + ["id1:", "id2:", "id3:"], + ["nl_f", "nll", "nl_r"], + ["nr_f", "nrr", "nr_r"], + ], +}, +} diff --git a/examples/transformed_jbeam/fender-after-frame-cfg-default.jbeam b/examples/transformed_jbeam/fender-after-frame-cfg-default.jbeam index 48fdcb37..6eb07612 100644 --- a/examples/transformed_jbeam/fender-after-frame-cfg-default.jbeam +++ b/examples/transformed_jbeam/fender-after-frame-cfg-default.jbeam @@ -23,7 +23,7 @@ // Left side {"group" : "cot_fender_l"}, - ["bfl_f", 0.959, -1.762, 0.576], + ["bfl0", 0.959, -1.762, 0.576], ["bfl1", 0.855, -1.788, 0.707], ["bfl2", 0.739, -1.845, 0.716], ["bfl3", 0.948, -1.435, 0.730], @@ -37,7 +37,7 @@ // Right side {"group" : "cot_fender_r"}, - ["bfr_f", -0.906, -1.737, 0.578], + ["bfr0", -0.906, -1.737, 0.578], ["bfr1", -0.807, -1.769, 0.707], ["bfr2", -0.691, -1.829, 0.716], ["bfr3", -0.890, -1.409, 0.729], diff --git a/examples/transformed_jbeam/fender-after-frame-cfg-example.jbeam b/examples/transformed_jbeam/fender-after-frame-cfg-example.jbeam index 48fdcb37..6eb07612 100644 --- a/examples/transformed_jbeam/fender-after-frame-cfg-example.jbeam +++ b/examples/transformed_jbeam/fender-after-frame-cfg-example.jbeam @@ -23,7 +23,7 @@ // Left side {"group" : "cot_fender_l"}, - ["bfl_f", 0.959, -1.762, 0.576], + ["bfl0", 0.959, -1.762, 0.576], ["bfl1", 0.855, -1.788, 0.707], ["bfl2", 0.739, -1.845, 0.716], ["bfl3", 0.948, -1.435, 0.730], @@ -37,7 +37,7 @@ // Right side {"group" : "cot_fender_r"}, - ["bfr_f", -0.906, -1.737, 0.578], + ["bfr0", -0.906, -1.737, 0.578], ["bfr1", -0.807, -1.769, 0.707], ["bfr2", -0.691, -1.829, 0.716], ["bfr3", -0.890, -1.409, 0.729], diff --git a/examples/transformed_jbeam/fender-cfg-default.jbeam b/examples/transformed_jbeam/fender-cfg-default.jbeam index ddc92ab3..42840269 100644 --- a/examples/transformed_jbeam/fender-cfg-default.jbeam +++ b/examples/transformed_jbeam/fender-cfg-default.jbeam @@ -14,51 +14,51 @@ }, "slotType" : "cot_fender", "nodes" : [ - ["id", "posX", "posY", "posZ"], + ["id", "posX", "posY", "posZ"], {"frictionCoef" : 0.7}, {"nodeMaterial" : "|NM_METAL"}, + + // Left side {"collision" : true}, {"group" : "cot_fender_l"}, {"nodeWeight" : 0.65}, {"selfCollision" : true}, - ["bfl0", 0.739, -1.845, 0.716], - ["bfl1", 0.855, -1.788, 0.707], - ["bfl2", 0.948, -1.435, 0.730], - ["bfl3", 0.756, -1.413, 0.843], - ["bfl4", 0.963, -1.024, 0.112], - ["bfl5", 0.964, -1.072, 0.507], - ["bfl6", 0.778, -1.008, 0.873], - ["bfl7", 0.987, -0.743, 0.109], - ["bfl8", 0.987, -0.744, 0.494], - ["bfl9", 0.812, -0.759, 0.896], - ["bfr0", -0.691, -1.829, 0.716], - ["bfr1", -0.807, -1.769, 0.707], - ["bfr2", -0.890, -1.409, 0.729], - ["bfr3", -0.700, -1.397, 0.843], - ["bfr4", -0.899, -1.005, 0.112], - ["bfr5", -0.900, -1.053, 0.508], - ["bfr6", -0.715, -0.991, 0.873], - ["bfr7", -0.916, -0.742, 0.112], - ["bfr8", -0.917, -0.746, 0.494], - ["bfr9", -0.734, -0.746, 0.888], + ["bfl0", 0.739, -1.845, 0.716], + ["bfl1", 0.959, -1.762, 0.576], + ["bfl2", 0.855, -1.788, 0.707], + ["bfl3", 0.948, -1.435, 0.730], + ["bfl4", 0.756, -1.413, 0.843], + ["bfl5", 0.963, -1.024, 0.112], + ["bfl6", 0.964, -1.072, 0.507], + ["bfl7", 0.778, -1.008, 0.873], + ["bfl8", 0.987, -0.743, 0.109], + ["bfl9", 0.987, -0.744, 0.494], + ["bfl10", 0.812, -0.759, 0.896], + + // Right side + {"group" : "cot_fender_r"}, + ["bfr0", -0.691, -1.829, 0.716], + ["bfr1", -0.906, -1.737, 0.578], + ["bfr2", -0.807, -1.769, 0.707], + ["bfr3", -0.890, -1.409, 0.729], + ["bfr4", -0.700, -1.397, 0.843], + ["bfr5", -0.899, -1.005, 0.112], + ["bfr6", -0.900, -1.053, 0.508], + ["bfr7", -0.715, -0.991, 0.873], + ["bfr8", -0.916, -0.742, 0.112], + ["bfr9", -0.917, -0.746, 0.494], + ["bfr10", -0.734, -0.746, 0.888], // Support nodes {"collision" : false}, {"group" : ""}, {"nodeWeight" : 1.2}, {"selfCollision" : false}, - ["bfsl", 0.684, -1.079, 0.507], - ["bfsr", -0.623, -1.064, 0.507], - {"collision" : true}, - {"group" : "cot_fender_l"}, - {"nodeWeight" : 0.65}, - {"selfCollision" : true}, - ["bfl_fsl", 0.959, -1.762, 0.576], - {"group" : "cot_fender_r"}, - ["bfr_fsr", -0.906, -1.737, 0.578], + ["bfsl", 0.684, -1.079, 0.507], + ["bfsr", -0.623, -1.064, 0.507], ], "beams" : [ - ["id1:", "id2:"], + ["id1:", "id2:"], // Structural beams // beamSpring: 451000, beamDamp: 50 @@ -70,100 +70,100 @@ {"beamDeform" : 6000}, // Front - ["bfr0", "bfr3"], - ["bfr0", "bfr1"], - ["bfr1", "bfr3"], - ["bfl3", "bfl0"], - ["bfl0", "bfl1"], - ["bfl0", "bfl1"], - ["bfl3", "bfl2"], - ["bfl0", "bfl2"], - ["bfr1", "bfr0"], - ["bfr3", "bfr2"], - ["bfr0", "bfr2"], - ["bfl3", "bfl1"], + ["bfr0", "bfr4"], + ["bfr0", "bfr2"], + ["bfr2", "bfr4"], + ["bfl4", "bfl0"], + ["bfl0", "bfl2"], + ["bfl1", "bfl2"], + ["bfl4", "bfl3"], + ["bfl1", "bfl3"], + ["bfr2", "bfr1"], + ["bfr4", "bfr3"], + ["bfr1", "bfr3"], + ["bfl4", "bfl2"], // Middle {"beamDeform" : 12000}, - ["bfl5", "bfl2"], - ["bfr5", "bfr2"], - ["bfl3", "bfl6"], - ["bfr3", "bfr6"], + ["bfl6", "bfl3"], + ["bfr6", "bfr3"], + ["bfl4", "bfl7"], + ["bfr4", "bfr7"], // Rear - ["bfl6", "bfl9"], - ["bfl4", "bfl5"], - ["bfl6", "bfl5"], - ["bfl4", "bfl7"], - ["bfl5", "bfl8"], - ["bfl7", "bfl8"], - ["bfl9", "bfl8"], - ["bfr6", "bfr9"], - ["bfr5", "bfr4"], - ["bfr5", "bfr6"], - ["bfr4", "bfr7"], - ["bfr5", "bfr8"], - ["bfr7", "bfr8"], - ["bfr9", "bfr8"], + ["bfl7", "bfl10"], + ["bfl5", "bfl6"], + ["bfl7", "bfl6"], + ["bfl5", "bfl8"], + ["bfl6", "bfl9"], + ["bfl8", "bfl9"], + ["bfl10", "bfl9"], + ["bfr7", "bfr10"], + ["bfr6", "bfr5"], + ["bfr6", "bfr7"], + ["bfr5", "bfr8"], + ["bfr6", "bfr9"], + ["bfr8", "bfr9"], + ["bfr10", "bfr9"], // Crossing beams {"deformLimitExpansion" : ""}, // Front - ["bfl0", "bfl3"], - ["bfl3", "bfl5"], - ["bfl6", "bfl2"], - ["bfr3", "bfr5"], - ["bfr6", "bfr2"], - ["bfl1", "bfl2"], - ["bfr2", "bfr1"], - ["bfr3", "bfr0"], + ["bfl1", "bfl4"], + ["bfl4", "bfl6"], + ["bfl7", "bfl3"], + ["bfr4", "bfr6"], + ["bfr7", "bfr3"], + ["bfl2", "bfl3"], + ["bfr3", "bfr2"], + ["bfr4", "bfr1"], // Rear - ["bfl5", "bfl9"], - ["bfl6", "bfl8"], - ["bfl4", "bfl8"], - ["bfl5", "bfl7"], - ["bfr5", "bfr9"], - ["bfr6", "bfr8"], - ["bfr4", "bfr8"], - ["bfr5", "bfr7"], + ["bfl6", "bfl10"], + ["bfl7", "bfl9"], + ["bfl5", "bfl9"], + ["bfl6", "bfl8"], + ["bfr6", "bfr10"], + ["bfr7", "bfr9"], + ["bfr5", "bfr9"], + ["bfr6", "bfr8"], // Support beams - ["bfl0", "bfsl"], - ["bfl5", "bfsl"], - ["bfl4", "bfsl"], - ["bfl2", "bfsl"], - ["bfl0", "bfsl"], - ["bfl1", "bfsl"], - ["bfl3", "bfsl"], - ["bfl6", "bfsl"], - ["bfl7", "bfsl"], - ["bfl8", "bfsl"], - ["bfl9", "bfsl"], - ["bfr0", "bfsr"], - ["bfr5", "bfsr"], - ["bfr4", "bfsr"], - ["bfr2", "bfsr"], - ["bfr0", "bfsr"], - ["bfr1", "bfsr"], - ["bfr3", "bfsr"], - ["bfr6", "bfsr"], - ["bfr7", "bfsr"], - ["bfr8", "bfsr"], - ["bfr9", "bfsr"], + ["bfl0", "bfsl"], + ["bfl6", "bfsl"], + ["bfl5", "bfsl"], + ["bfl3", "bfsl"], + ["bfl1", "bfsl"], + ["bfl2", "bfsl"], + ["bfl4", "bfsl"], + ["bfl7", "bfsl"], + ["bfl8", "bfsl"], + ["bfl9", "bfsl"], + ["bfl10", "bfsl"], + ["bfr0", "bfsr"], + ["bfr6", "bfsr"], + ["bfr5", "bfsr"], + ["bfr3", "bfsr"], + ["bfr1", "bfsr"], + ["bfr2", "bfsr"], + ["bfr4", "bfsr"], + ["bfr7", "bfsr"], + ["bfr8", "bfsr"], + ["bfr9", "bfsr"], + ["bfr10", "bfsr"], // Front rigid {"beamSpring" : 350000, "beamDamp" : 115}, {"beamDeform" : 900}, // Left side - ["bfl2", "bfl8"], - ["bfl3", "bfl9"], + ["bfl3", "bfl9"], + ["bfl4", "bfl10"], // Right side - ["bfr2", "bfr8"], - ["bfr3", "bfr9"], + ["bfr3", "bfr9"], + ["bfr4", "bfr10"], // Attachment beams {"beamType" : "|NORMAL"}, @@ -177,36 +177,36 @@ // Front {"breakGroup" : "fender_l"}, {"breakGroupType" : 1}, - ["bfl0", "fr17"], - ["bfl0", "fr9"], - ["bfl1", "fr17"], - ["bfl1", "fr9"], - ["bfl0", "fr17"], - ["bfl0", "fr9"], + ["bfl1", "fr17"], + ["bfl1", "fr9"], + ["bfl2", "fr17"], + ["bfl2", "fr9"], + ["bfl0", "fr17"], + ["bfl0", "fr9"], // Middle {"beamDeform" : 18000}, {"beamStrength" : 4000}, {"breakGroupType" : 1}, - ["bfl2", "fr17"], - ["bfl3", "fr17"], - ["bfl3", "fr9"], + ["bfl3", "fr17"], + ["bfl4", "fr17"], + ["bfl4", "fr9"], {"breakGroupType" : 0}, // Rear {"beamStrength" : 16000, "beamDeform" : 12000}, - ["bfl4", "fr18"], - ["bfl4", "fr27"], - ["bfl5", "fr17"], - ["bfl5", "fr21"], - ["bfl6", "fr17"], - ["bfl6", "fr29"], - ["bfl7", "fr18"], - ["bfl7", "fr27"], - ["bfl8", "fr28"], - ["bfl8", "fr21"], - ["bfl9", "fr17"], - ["bfl9", "fr29"], + ["bfl5", "fr18"], + ["bfl5", "fr27"], + ["bfl6", "fr17"], + ["bfl6", "fr21"], + ["bfl7", "fr17"], + ["bfl7", "fr29"], + ["bfl8", "fr18"], + ["bfl8", "fr27"], + ["bfl9", "fr28"], + ["bfl9", "fr21"], + ["bfl10", "fr17"], + ["bfl10", "fr29"], // Right side @@ -214,86 +214,86 @@ {"beamStrength" : 20000, "beamDeform" : 1000}, {"breakGroup" : "fender_r"}, {"breakGroupType" : 1}, - ["bfl0", "rl_f8"], - ["bfl0", "rl_f6"], - ["bfl1", "rl_f6"], - ["bfl0", "rl_f6"], - ["bfr0", "rl_f9"], - ["bfr0", "rl_f7"], - ["bfr1", "rl_f7"], - ["bfr0", "rl_f7"], + ["bfl1", "rl_f8"], + ["bfl1", "rl_f6"], + ["bfl2", "rl_f6"], + ["bfl0", "rl_f6"], + ["bfr1", "rl_f9"], + ["bfr1", "rl_f7"], + ["bfr2", "rl_f7"], + ["bfr0", "rl_f7"], // Middle {"beamDeform" : 18000}, {"beamStrength" : 4000}, {"breakGroupType" : 1}, - ["bfl2", "rl_f10"], - ["bfl2", "rl_f11"], - ["bfl3", "rl_f11"], - ["bfr2", "rl_f12"], - ["bfr2", "rl_f13"], - ["bfr3", "rl_f13"], + ["bfl3", "rl_f10"], + ["bfl3", "rl_f11"], + ["bfl4", "rl_f11"], + ["bfr3", "rl_f12"], + ["bfr3", "rl_f13"], + ["bfr4", "rl_f13"], {"breakGroupType" : 0}, // Rear {"beamStrength" : 16000, "beamDeform" : 12000}, - ["bfl4", "rl_f15"], - ["bfl4", "rl_f16"], - ["bfl5", "rl_f15"], - ["bfl5", "rl_f16"], - ["bfl7", "rl_f15"], - ["bfl7", "rl_f16"], - ["bfl8", "rl_f15"], - ["bfl8", "rl_f16"], - ["bfl6", "rl_f16"], - ["bfl9", "rl_f16"], - ["bfr4", "rl_f17"], - ["bfr4", "rl_f18"], - ["bfr5", "rl_f17"], - ["bfr5", "rl_f18"], - ["bfr7", "rl_f17"], - ["bfr7", "rl_f18"], - ["bfr8", "rl_f17"], - ["bfr8", "rl_f18"], - ["bfr6", "rl_f18"], - ["bfr9", "rl_f18"], + ["bfl5", "rl_f15"], + ["bfl5", "rl_f16"], + ["bfl6", "rl_f15"], + ["bfl6", "rl_f16"], + ["bfl8", "rl_f15"], + ["bfl8", "rl_f16"], + ["bfl9", "rl_f15"], + ["bfl9", "rl_f16"], + ["bfl7", "rl_f16"], + ["bfl10", "rl_f16"], + ["bfr5", "rl_f17"], + ["bfr5", "rl_f18"], + ["bfr6", "rl_f17"], + ["bfr6", "rl_f18"], + ["bfr8", "rl_f17"], + ["bfr8", "rl_f18"], + ["bfr9", "rl_f17"], + ["bfr9", "rl_f18"], + ["bfr7", "rl_f18"], + ["bfr10", "rl_f18"], {"beamStrength" : 16000, "beamDeform" : "FLT_MAX"}, // Body // Left side {"breakGroup" : "fender_l"}, - ["bfl7", "mbl0"], - ["bfl8", "mbl1"], - ["bfl9", "mbl2"], + ["bfl8", "mbl0"], + ["bfl9", "mbl1"], + ["bfl10", "mbl2"], // Right side {"breakGroup" : "fender_r"}, - ["bfr7", "mbr0"], - ["bfr8", "mbr1"], - ["bfr9", "mbr2"], + ["bfr8", "mbr0"], + ["bfr9", "mbr1"], + ["bfr10", "mbr2"], {"breakGroup" : "", "breakGroupType" : "", "beamType" : "|NORMAL"}, ], "triangles" : [ - ["id1:", "id2:", "id3:"], - ["bfl2", "bfl3", "bfl1"], - ["bfl0", "bfl2", "bfl1"], - ["bfl3", "bfl0", "bfl1"], - ["bfl3", "bfl2", "bfl6"], - ["bfl5", "bfl6", "bfl2"], - ["bfl9", "bfl6", "bfl5"], - ["bfl8", "bfl9", "bfl5"], - ["bfl4", "bfl7", "bfl5"], - ["bfl8", "bfl5", "bfl7"], - ["bfr3", "bfr1", "bfr0"], - ["bfr2", "bfr1", "bfr3"], - ["bfr0", "bfr1", "bfr2"], - ["bfr5", "bfr2", "bfr6"], - ["bfr3", "bfr6", "bfr2"], - ["bfr9", "bfr5", "bfr6"], - ["bfr8", "bfr5", "bfr9"], - ["bfr8", "bfr7", "bfr5"], - ["bfr4", "bfr5", "bfr7"], + ["id1:", "id2:", "id3:"], + ["bfl3", "bfl4", "bfl2"], + ["bfl1", "bfl3", "bfl2"], + ["bfl4", "bfl0", "bfl2"], + ["bfl4", "bfl3", "bfl7"], + ["bfl6", "bfl7", "bfl3"], + ["bfl10", "bfl7", "bfl6"], + ["bfl9", "bfl10", "bfl6"], + ["bfl5", "bfl8", "bfl6"], + ["bfl9", "bfl6", "bfl8"], + ["bfr4", "bfr2", "bfr0"], + ["bfr3", "bfr2", "bfr4"], + ["bfr1", "bfr2", "bfr3"], + ["bfr6", "bfr3", "bfr7"], + ["bfr4", "bfr7", "bfr3"], + ["bfr10", "bfr6", "bfr7"], + ["bfr9", "bfr6", "bfr10"], + ["bfr9", "bfr8", "bfr6"], + ["bfr5", "bfr6", "bfr8"], ], "flexbodies" : [ ["mesh", "[group]:", "nonFlexMaterials"], diff --git a/examples/transformed_jbeam/fender-cfg-example.jbeam b/examples/transformed_jbeam/fender-cfg-example.jbeam index ddc92ab3..42840269 100644 --- a/examples/transformed_jbeam/fender-cfg-example.jbeam +++ b/examples/transformed_jbeam/fender-cfg-example.jbeam @@ -14,51 +14,51 @@ }, "slotType" : "cot_fender", "nodes" : [ - ["id", "posX", "posY", "posZ"], + ["id", "posX", "posY", "posZ"], {"frictionCoef" : 0.7}, {"nodeMaterial" : "|NM_METAL"}, + + // Left side {"collision" : true}, {"group" : "cot_fender_l"}, {"nodeWeight" : 0.65}, {"selfCollision" : true}, - ["bfl0", 0.739, -1.845, 0.716], - ["bfl1", 0.855, -1.788, 0.707], - ["bfl2", 0.948, -1.435, 0.730], - ["bfl3", 0.756, -1.413, 0.843], - ["bfl4", 0.963, -1.024, 0.112], - ["bfl5", 0.964, -1.072, 0.507], - ["bfl6", 0.778, -1.008, 0.873], - ["bfl7", 0.987, -0.743, 0.109], - ["bfl8", 0.987, -0.744, 0.494], - ["bfl9", 0.812, -0.759, 0.896], - ["bfr0", -0.691, -1.829, 0.716], - ["bfr1", -0.807, -1.769, 0.707], - ["bfr2", -0.890, -1.409, 0.729], - ["bfr3", -0.700, -1.397, 0.843], - ["bfr4", -0.899, -1.005, 0.112], - ["bfr5", -0.900, -1.053, 0.508], - ["bfr6", -0.715, -0.991, 0.873], - ["bfr7", -0.916, -0.742, 0.112], - ["bfr8", -0.917, -0.746, 0.494], - ["bfr9", -0.734, -0.746, 0.888], + ["bfl0", 0.739, -1.845, 0.716], + ["bfl1", 0.959, -1.762, 0.576], + ["bfl2", 0.855, -1.788, 0.707], + ["bfl3", 0.948, -1.435, 0.730], + ["bfl4", 0.756, -1.413, 0.843], + ["bfl5", 0.963, -1.024, 0.112], + ["bfl6", 0.964, -1.072, 0.507], + ["bfl7", 0.778, -1.008, 0.873], + ["bfl8", 0.987, -0.743, 0.109], + ["bfl9", 0.987, -0.744, 0.494], + ["bfl10", 0.812, -0.759, 0.896], + + // Right side + {"group" : "cot_fender_r"}, + ["bfr0", -0.691, -1.829, 0.716], + ["bfr1", -0.906, -1.737, 0.578], + ["bfr2", -0.807, -1.769, 0.707], + ["bfr3", -0.890, -1.409, 0.729], + ["bfr4", -0.700, -1.397, 0.843], + ["bfr5", -0.899, -1.005, 0.112], + ["bfr6", -0.900, -1.053, 0.508], + ["bfr7", -0.715, -0.991, 0.873], + ["bfr8", -0.916, -0.742, 0.112], + ["bfr9", -0.917, -0.746, 0.494], + ["bfr10", -0.734, -0.746, 0.888], // Support nodes {"collision" : false}, {"group" : ""}, {"nodeWeight" : 1.2}, {"selfCollision" : false}, - ["bfsl", 0.684, -1.079, 0.507], - ["bfsr", -0.623, -1.064, 0.507], - {"collision" : true}, - {"group" : "cot_fender_l"}, - {"nodeWeight" : 0.65}, - {"selfCollision" : true}, - ["bfl_fsl", 0.959, -1.762, 0.576], - {"group" : "cot_fender_r"}, - ["bfr_fsr", -0.906, -1.737, 0.578], + ["bfsl", 0.684, -1.079, 0.507], + ["bfsr", -0.623, -1.064, 0.507], ], "beams" : [ - ["id1:", "id2:"], + ["id1:", "id2:"], // Structural beams // beamSpring: 451000, beamDamp: 50 @@ -70,100 +70,100 @@ {"beamDeform" : 6000}, // Front - ["bfr0", "bfr3"], - ["bfr0", "bfr1"], - ["bfr1", "bfr3"], - ["bfl3", "bfl0"], - ["bfl0", "bfl1"], - ["bfl0", "bfl1"], - ["bfl3", "bfl2"], - ["bfl0", "bfl2"], - ["bfr1", "bfr0"], - ["bfr3", "bfr2"], - ["bfr0", "bfr2"], - ["bfl3", "bfl1"], + ["bfr0", "bfr4"], + ["bfr0", "bfr2"], + ["bfr2", "bfr4"], + ["bfl4", "bfl0"], + ["bfl0", "bfl2"], + ["bfl1", "bfl2"], + ["bfl4", "bfl3"], + ["bfl1", "bfl3"], + ["bfr2", "bfr1"], + ["bfr4", "bfr3"], + ["bfr1", "bfr3"], + ["bfl4", "bfl2"], // Middle {"beamDeform" : 12000}, - ["bfl5", "bfl2"], - ["bfr5", "bfr2"], - ["bfl3", "bfl6"], - ["bfr3", "bfr6"], + ["bfl6", "bfl3"], + ["bfr6", "bfr3"], + ["bfl4", "bfl7"], + ["bfr4", "bfr7"], // Rear - ["bfl6", "bfl9"], - ["bfl4", "bfl5"], - ["bfl6", "bfl5"], - ["bfl4", "bfl7"], - ["bfl5", "bfl8"], - ["bfl7", "bfl8"], - ["bfl9", "bfl8"], - ["bfr6", "bfr9"], - ["bfr5", "bfr4"], - ["bfr5", "bfr6"], - ["bfr4", "bfr7"], - ["bfr5", "bfr8"], - ["bfr7", "bfr8"], - ["bfr9", "bfr8"], + ["bfl7", "bfl10"], + ["bfl5", "bfl6"], + ["bfl7", "bfl6"], + ["bfl5", "bfl8"], + ["bfl6", "bfl9"], + ["bfl8", "bfl9"], + ["bfl10", "bfl9"], + ["bfr7", "bfr10"], + ["bfr6", "bfr5"], + ["bfr6", "bfr7"], + ["bfr5", "bfr8"], + ["bfr6", "bfr9"], + ["bfr8", "bfr9"], + ["bfr10", "bfr9"], // Crossing beams {"deformLimitExpansion" : ""}, // Front - ["bfl0", "bfl3"], - ["bfl3", "bfl5"], - ["bfl6", "bfl2"], - ["bfr3", "bfr5"], - ["bfr6", "bfr2"], - ["bfl1", "bfl2"], - ["bfr2", "bfr1"], - ["bfr3", "bfr0"], + ["bfl1", "bfl4"], + ["bfl4", "bfl6"], + ["bfl7", "bfl3"], + ["bfr4", "bfr6"], + ["bfr7", "bfr3"], + ["bfl2", "bfl3"], + ["bfr3", "bfr2"], + ["bfr4", "bfr1"], // Rear - ["bfl5", "bfl9"], - ["bfl6", "bfl8"], - ["bfl4", "bfl8"], - ["bfl5", "bfl7"], - ["bfr5", "bfr9"], - ["bfr6", "bfr8"], - ["bfr4", "bfr8"], - ["bfr5", "bfr7"], + ["bfl6", "bfl10"], + ["bfl7", "bfl9"], + ["bfl5", "bfl9"], + ["bfl6", "bfl8"], + ["bfr6", "bfr10"], + ["bfr7", "bfr9"], + ["bfr5", "bfr9"], + ["bfr6", "bfr8"], // Support beams - ["bfl0", "bfsl"], - ["bfl5", "bfsl"], - ["bfl4", "bfsl"], - ["bfl2", "bfsl"], - ["bfl0", "bfsl"], - ["bfl1", "bfsl"], - ["bfl3", "bfsl"], - ["bfl6", "bfsl"], - ["bfl7", "bfsl"], - ["bfl8", "bfsl"], - ["bfl9", "bfsl"], - ["bfr0", "bfsr"], - ["bfr5", "bfsr"], - ["bfr4", "bfsr"], - ["bfr2", "bfsr"], - ["bfr0", "bfsr"], - ["bfr1", "bfsr"], - ["bfr3", "bfsr"], - ["bfr6", "bfsr"], - ["bfr7", "bfsr"], - ["bfr8", "bfsr"], - ["bfr9", "bfsr"], + ["bfl0", "bfsl"], + ["bfl6", "bfsl"], + ["bfl5", "bfsl"], + ["bfl3", "bfsl"], + ["bfl1", "bfsl"], + ["bfl2", "bfsl"], + ["bfl4", "bfsl"], + ["bfl7", "bfsl"], + ["bfl8", "bfsl"], + ["bfl9", "bfsl"], + ["bfl10", "bfsl"], + ["bfr0", "bfsr"], + ["bfr6", "bfsr"], + ["bfr5", "bfsr"], + ["bfr3", "bfsr"], + ["bfr1", "bfsr"], + ["bfr2", "bfsr"], + ["bfr4", "bfsr"], + ["bfr7", "bfsr"], + ["bfr8", "bfsr"], + ["bfr9", "bfsr"], + ["bfr10", "bfsr"], // Front rigid {"beamSpring" : 350000, "beamDamp" : 115}, {"beamDeform" : 900}, // Left side - ["bfl2", "bfl8"], - ["bfl3", "bfl9"], + ["bfl3", "bfl9"], + ["bfl4", "bfl10"], // Right side - ["bfr2", "bfr8"], - ["bfr3", "bfr9"], + ["bfr3", "bfr9"], + ["bfr4", "bfr10"], // Attachment beams {"beamType" : "|NORMAL"}, @@ -177,36 +177,36 @@ // Front {"breakGroup" : "fender_l"}, {"breakGroupType" : 1}, - ["bfl0", "fr17"], - ["bfl0", "fr9"], - ["bfl1", "fr17"], - ["bfl1", "fr9"], - ["bfl0", "fr17"], - ["bfl0", "fr9"], + ["bfl1", "fr17"], + ["bfl1", "fr9"], + ["bfl2", "fr17"], + ["bfl2", "fr9"], + ["bfl0", "fr17"], + ["bfl0", "fr9"], // Middle {"beamDeform" : 18000}, {"beamStrength" : 4000}, {"breakGroupType" : 1}, - ["bfl2", "fr17"], - ["bfl3", "fr17"], - ["bfl3", "fr9"], + ["bfl3", "fr17"], + ["bfl4", "fr17"], + ["bfl4", "fr9"], {"breakGroupType" : 0}, // Rear {"beamStrength" : 16000, "beamDeform" : 12000}, - ["bfl4", "fr18"], - ["bfl4", "fr27"], - ["bfl5", "fr17"], - ["bfl5", "fr21"], - ["bfl6", "fr17"], - ["bfl6", "fr29"], - ["bfl7", "fr18"], - ["bfl7", "fr27"], - ["bfl8", "fr28"], - ["bfl8", "fr21"], - ["bfl9", "fr17"], - ["bfl9", "fr29"], + ["bfl5", "fr18"], + ["bfl5", "fr27"], + ["bfl6", "fr17"], + ["bfl6", "fr21"], + ["bfl7", "fr17"], + ["bfl7", "fr29"], + ["bfl8", "fr18"], + ["bfl8", "fr27"], + ["bfl9", "fr28"], + ["bfl9", "fr21"], + ["bfl10", "fr17"], + ["bfl10", "fr29"], // Right side @@ -214,86 +214,86 @@ {"beamStrength" : 20000, "beamDeform" : 1000}, {"breakGroup" : "fender_r"}, {"breakGroupType" : 1}, - ["bfl0", "rl_f8"], - ["bfl0", "rl_f6"], - ["bfl1", "rl_f6"], - ["bfl0", "rl_f6"], - ["bfr0", "rl_f9"], - ["bfr0", "rl_f7"], - ["bfr1", "rl_f7"], - ["bfr0", "rl_f7"], + ["bfl1", "rl_f8"], + ["bfl1", "rl_f6"], + ["bfl2", "rl_f6"], + ["bfl0", "rl_f6"], + ["bfr1", "rl_f9"], + ["bfr1", "rl_f7"], + ["bfr2", "rl_f7"], + ["bfr0", "rl_f7"], // Middle {"beamDeform" : 18000}, {"beamStrength" : 4000}, {"breakGroupType" : 1}, - ["bfl2", "rl_f10"], - ["bfl2", "rl_f11"], - ["bfl3", "rl_f11"], - ["bfr2", "rl_f12"], - ["bfr2", "rl_f13"], - ["bfr3", "rl_f13"], + ["bfl3", "rl_f10"], + ["bfl3", "rl_f11"], + ["bfl4", "rl_f11"], + ["bfr3", "rl_f12"], + ["bfr3", "rl_f13"], + ["bfr4", "rl_f13"], {"breakGroupType" : 0}, // Rear {"beamStrength" : 16000, "beamDeform" : 12000}, - ["bfl4", "rl_f15"], - ["bfl4", "rl_f16"], - ["bfl5", "rl_f15"], - ["bfl5", "rl_f16"], - ["bfl7", "rl_f15"], - ["bfl7", "rl_f16"], - ["bfl8", "rl_f15"], - ["bfl8", "rl_f16"], - ["bfl6", "rl_f16"], - ["bfl9", "rl_f16"], - ["bfr4", "rl_f17"], - ["bfr4", "rl_f18"], - ["bfr5", "rl_f17"], - ["bfr5", "rl_f18"], - ["bfr7", "rl_f17"], - ["bfr7", "rl_f18"], - ["bfr8", "rl_f17"], - ["bfr8", "rl_f18"], - ["bfr6", "rl_f18"], - ["bfr9", "rl_f18"], + ["bfl5", "rl_f15"], + ["bfl5", "rl_f16"], + ["bfl6", "rl_f15"], + ["bfl6", "rl_f16"], + ["bfl8", "rl_f15"], + ["bfl8", "rl_f16"], + ["bfl9", "rl_f15"], + ["bfl9", "rl_f16"], + ["bfl7", "rl_f16"], + ["bfl10", "rl_f16"], + ["bfr5", "rl_f17"], + ["bfr5", "rl_f18"], + ["bfr6", "rl_f17"], + ["bfr6", "rl_f18"], + ["bfr8", "rl_f17"], + ["bfr8", "rl_f18"], + ["bfr9", "rl_f17"], + ["bfr9", "rl_f18"], + ["bfr7", "rl_f18"], + ["bfr10", "rl_f18"], {"beamStrength" : 16000, "beamDeform" : "FLT_MAX"}, // Body // Left side {"breakGroup" : "fender_l"}, - ["bfl7", "mbl0"], - ["bfl8", "mbl1"], - ["bfl9", "mbl2"], + ["bfl8", "mbl0"], + ["bfl9", "mbl1"], + ["bfl10", "mbl2"], // Right side {"breakGroup" : "fender_r"}, - ["bfr7", "mbr0"], - ["bfr8", "mbr1"], - ["bfr9", "mbr2"], + ["bfr8", "mbr0"], + ["bfr9", "mbr1"], + ["bfr10", "mbr2"], {"breakGroup" : "", "breakGroupType" : "", "beamType" : "|NORMAL"}, ], "triangles" : [ - ["id1:", "id2:", "id3:"], - ["bfl2", "bfl3", "bfl1"], - ["bfl0", "bfl2", "bfl1"], - ["bfl3", "bfl0", "bfl1"], - ["bfl3", "bfl2", "bfl6"], - ["bfl5", "bfl6", "bfl2"], - ["bfl9", "bfl6", "bfl5"], - ["bfl8", "bfl9", "bfl5"], - ["bfl4", "bfl7", "bfl5"], - ["bfl8", "bfl5", "bfl7"], - ["bfr3", "bfr1", "bfr0"], - ["bfr2", "bfr1", "bfr3"], - ["bfr0", "bfr1", "bfr2"], - ["bfr5", "bfr2", "bfr6"], - ["bfr3", "bfr6", "bfr2"], - ["bfr9", "bfr5", "bfr6"], - ["bfr8", "bfr5", "bfr9"], - ["bfr8", "bfr7", "bfr5"], - ["bfr4", "bfr5", "bfr7"], + ["id1:", "id2:", "id3:"], + ["bfl3", "bfl4", "bfl2"], + ["bfl1", "bfl3", "bfl2"], + ["bfl4", "bfl0", "bfl2"], + ["bfl4", "bfl3", "bfl7"], + ["bfl6", "bfl7", "bfl3"], + ["bfl10", "bfl7", "bfl6"], + ["bfl9", "bfl10", "bfl6"], + ["bfl5", "bfl8", "bfl6"], + ["bfl9", "bfl6", "bfl8"], + ["bfr4", "bfr2", "bfr0"], + ["bfr3", "bfr2", "bfr4"], + ["bfr1", "bfr2", "bfr3"], + ["bfr6", "bfr3", "bfr7"], + ["bfr4", "bfr7", "bfr3"], + ["bfr10", "bfr6", "bfr7"], + ["bfr9", "bfr6", "bfr10"], + ["bfr9", "bfr8", "bfr6"], + ["bfr5", "bfr6", "bfr8"], ], "flexbodies" : [ ["mesh", "[group]:", "nonFlexMaterials"], diff --git a/test-extra/transformation/Spec.hs b/test-extra/transformation/Spec.hs index e5a0cfe5..e7c00d8c 100644 --- a/test-extra/transformation/Spec.hs +++ b/test-extra/transformation/Spec.hs @@ -4,6 +4,7 @@ module Spec ( import Data.List (isPrefixOf, isSuffixOf) import Data.Map qualified as M +import Data.Set (Set) import Data.Set qualified as S import Data.Text (Text) import Data.Text qualified as T @@ -108,6 +109,46 @@ vertexPositionsInOrder topNode = , Just (Number yNum) <- [inner V.!? 2] ] +{- | Every vertex coordinate in a top node's "nodes" section. Positions +survive renaming, so they identify a vertex across a transform. +-} +vertexCoordinates :: Node -> Set (Double, Double, Double) +vertexCoordinates topNode = + case NP.queryNodes nodesQuery topNode >>= NP.expectArray nodesQuery of + Left _ -> S.empty + Right rows -> + S.fromList + [ (realToFrac (nvValue x), realToFrac (nvValue y), realToFrac (nvValue z)) + | row <- V.toList rows + , Just inner <- [expectArray row] + , Just (String name) <- [inner V.!? 0] + , name /= "id" + , Just (Number x) <- [inner V.!? 1] + , Just (Number y) <- [inner V.!? 2] + , Just (Number z) <- [inner V.!? 3] + ] + +{- | Names ending in a letter rather than a digit all map to the same +SupportKey, so an insert that replaced instead of merged used to drop +every group but the last. +-} +letterEndingNodesFixture :: FilePath +letterEndingNodesFixture = + "examples/regression_jbeam/letter-ending-nodes-repro.jbeam" + +letterEndingNodesSpec :: Spec +letterEndingNodesSpec = + describe "letter-ending node names" + . it "keeps every vertex through a transform" + $ do + topNode <- parseJbeamFile letterEndingNodesFixture + let expected = vertexCoordinates topNode + expected `shouldNotBe` S.empty + case transform M.empty newTransformationConfig topNode of + Left err -> expectationFailure ("transform failed: " ++ T.unpack err) + Right (_, _, _, resultNode) -> + vertexCoordinates resultNode `shouldBe` expected + {- | Three small hubs (nl0, nl10, nl20; front/mid/rear), each beamed to three of its own ordinary leaf nodes (see issue #215). At support-threshold 20 with 12 nodes in the group, thrCount = @@ -160,3 +201,4 @@ main = hspec $ do mapM_ (testInputFile "cfg-example" tfConfig) inputFiles beamValidationSpec supportRenameIdempotencySpec + letterEndingNodesSpec From e729f7709a3a32421d138b6b798b17171ad9de8c Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:57:12 +0200 Subject: [PATCH 4/8] Sort support vertices by the name they will get compareAV ordered support vertices by their current name prefix, and the current name is what the transformation is about to replace. The first pass sorted on the source prefixes (rl, rl_f, rl_r), the second on the names the first pass produced, so the section came out in a different order each time. Sort on the prefix the vertex is about to be given instead. It is a pure function of the name and the x coordinate, so it can be computed before the rename, and support prefixes are a fixed point of it, so both passes agree. Frame's support section settles after one pass. Suspension still moves, for an unrelated reason: all three of its support nodes collapse to the same prefix, so the tie falls through to the metadata, and metadata is not carried across vertex tree boundaries. --- .../transformed_jbeam/frame-cfg-default.jbeam | 2 +- .../transformed_jbeam/frame-cfg-example.jbeam | 2 +- .../JbeamEdit/Transformation.hs | 67 ++++++++++++------- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/examples/transformed_jbeam/frame-cfg-default.jbeam b/examples/transformed_jbeam/frame-cfg-default.jbeam index 1ce19b0e..794c841c 100644 --- a/examples/transformed_jbeam/frame-cfg-default.jbeam +++ b/examples/transformed_jbeam/frame-cfg-default.jbeam @@ -92,9 +92,9 @@ ["rlr7", -0.715, 0.791, 0.233], // Support nodes - ["rlsm", 0.053, -0.024, 0.578], // support ["rl_fsm", 0.053, -1.314, 0.382], // support for front ["rl_sm", 0.053, 1.710, 0.565], // support for rear + ["rlsm", 0.053, -0.024, 0.578], // support ], // --Beams-- "beams" : [ diff --git a/examples/transformed_jbeam/frame-cfg-example.jbeam b/examples/transformed_jbeam/frame-cfg-example.jbeam index 1ce19b0e..794c841c 100644 --- a/examples/transformed_jbeam/frame-cfg-example.jbeam +++ b/examples/transformed_jbeam/frame-cfg-example.jbeam @@ -92,9 +92,9 @@ ["rlr7", -0.715, 0.791, 0.233], // Support nodes - ["rlsm", 0.053, -0.024, 0.578], // support ["rl_fsm", 0.053, -1.314, 0.382], // support for front ["rl_sm", 0.053, 1.710, 0.565], // support for rear + ["rlsm", 0.053, -0.024, 0.578], // support ], // --Beams-- "beams" : [ diff --git a/src-extra/transformation/JbeamEdit/Transformation.hs b/src-extra/transformation/JbeamEdit/Transformation.hs index 4558a856..f302f843 100644 --- a/src-extra/transformation/JbeamEdit/Transformation.hs +++ b/src-extra/transformation/JbeamEdit/Transformation.hs @@ -187,6 +187,7 @@ moveSupportVertices newNames tfCfg connMap vsPerType = thr = ySortingThreshold tfCfg assignSupportNames = assignNames newNames brks SupportTree + supportKey = vertexPrefix newNames brks SupportTree vertexForest :: VertexForest vertexForest = @@ -201,7 +202,7 @@ moveSupportVertices newNames tfCfg connMap vsPerType = [sideComment SupportTree] ( snd . mapAccumL assignSupportNames M.empty - $ NE.sortBy (compareAV thr SupportTree) vs + $ NE.sortBy (compareAV supportKey thr SupportTree) vs ) ) ) @@ -333,12 +334,17 @@ treesOrder :: [VertexTreeType] treesOrder = [LeftTree, MiddleTree, RightTree, SupportTree] compareAV - :: Scientific -> VertexTreeType -> AnnotatedVertex -> AnnotatedVertex -> Ordering -compareAV thr treeType vertex1 vertex2 = + :: (Vertex -> Text) + -> Scientific + -> VertexTreeType + -> AnnotatedVertex + -> AnnotatedVertex + -> Ordering +compareAV supportKey thr treeType vertex1 vertex2 = let supportNameCompare = bool EQ - (on compare (dropIndex . vName . aVertex) vertex1 vertex2) + (on compare (supportKey . aVertex) vertex1 vertex2) (treeType == SupportTree) y1 = vY . aVertex $ vertex1 y2 = vY . aVertex $ vertex2 @@ -356,9 +362,9 @@ compareAV thr treeType vertex1 vertex2 = ] renameVertexId :: VertexTreeType -> Int -> Text -> Text -renameVertexId treeType idx vertexPrefix = +renameVertexId treeType idx prefix = let idx' = mwhen (treeType /= SupportTree || idx /= 0) (intToText idx) - in vertexPrefix <> idx' + in prefix <> idx' sideLetters :: String sideLetters = ['l', 'm', 'r'] @@ -380,6 +386,30 @@ dropSupportSuffix prefix = (T.init prefix) (T.length prefix > 2 && T.last prefix `elem` sideLetters) +{- | The name a vertex gets, before the running index is appended. Support +prefixes are a fixed point of this, which is what lets it double as a sort +key that survives a transform. +-} +vertexPrefix + :: UpdateNamesMap -> XGroupBreakpoints -> VertexTreeType -> Vertex -> Text +vertexPrefix newNames brks treeType v + | treeType == SupportTree = + updatedPrefix (dropSupportSuffix prefix) <> T.singleton 's' <> typeSpecific + | T.length prefix >= 3 + && T.last prefix' == 's' = + updatedPrefix (T.init prefix') <> typeSpecific + | T.length prefix >= 3 + && isLmr = + updatedPrefix prefix' <> typeSpecific + | otherwise = + updatedPrefix prefix <> typeSpecific + where + updatedPrefix cleanPrefix = M.findWithDefault cleanPrefix cleanPrefix newNames + prefix = dropIndex (vName v) + typeSpecific = either (const "") prefixForType (determineGroup brks v) + (prefix', lastChar) = fromMaybe (error "unreachable") (T.unsnoc prefix) + isLmr = lastChar `elem` sideLetters + assignNames :: UpdateNamesMap -> XGroupBreakpoints @@ -389,25 +419,9 @@ assignNames -> (Map Text Int, AnnotatedVertex) assignNames newNames brks treeType prefixMap av = let v = aVertex av - updatedPrefix cleanPrefix' = M.findWithDefault cleanPrefix' cleanPrefix' newNames - prefix = dropIndex (vName v) - typeSpecific = either (const "") prefixForType (determineGroup brks v) - (prefix', lastChar) = fromMaybe (error "unreachable") (T.unsnoc prefix) - isLmr = lastChar `elem` sideLetters - cleanPrefix - | treeType == SupportTree = - updatedPrefix (dropSupportSuffix prefix) <> T.singleton 's' <> typeSpecific - | T.length prefix >= 3 - && T.last prefix' == 's' = - updatedPrefix (T.init prefix') <> typeSpecific - | T.length prefix >= 3 - && isLmr = - updatedPrefix prefix' <> typeSpecific - | otherwise = - updatedPrefix prefix <> typeSpecific + cleanPrefix = vertexPrefix newNames brks treeType v lastIdx = M.findWithDefault 0 cleanPrefix prefixMap - newName = renameVertexId treeType lastIdx cleanPrefix - newVertex = v {vName = newName} + newVertex = v {vName = renameVertexId treeType lastIdx cleanPrefix} prefixMap' = M.insert cleanPrefix (lastIdx + 1) prefixMap in (prefixMap', av {aVertex = newVertex}) @@ -420,7 +434,10 @@ sortVertices sortVertices treeType newNames tfCfg (VertexTree comments vertices) = let thr = ySortingThreshold tfCfg brks = xGroupBreakpoints tfCfg - sortedGroups = NE.sortBy (compareAV thr treeType) vertices + sortedGroups = + NE.sortBy + (compareAV (vertexPrefix newNames brks treeType) thr treeType) + vertices renamedGroups = snd $ mapAccumL (assignNames newNames brks treeType) M.empty sortedGroups in VertexTree comments renamedGroups From 536a04f0482885e2c2355197e360f82f2a3587f0 Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:57:40 +0200 Subject: [PATCH 5/8] Keep comment order when a vertex tree is split breakVertices builds its accumulator back to front and reverses it before handing the finished tree back. The nodes it hands to the next tree, the metadata and comments sitting directly in front of the vertex that ended the current one, were never reversed, so they arrived backwards. With two comments in front of a group that means they swap places on every run, which is why frame's // Middle side and // prefix group rl_m kept trading positions and never settled. The first tree in a section is unaffected, it gets its comments from newVertexTree rather than from this hand-off, which is why only the second and later groups moved. --- .../JbeamEdit/Transformation/VertexExtraction.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-extra/transformation/JbeamEdit/Transformation/VertexExtraction.hs b/src-extra/transformation/JbeamEdit/Transformation/VertexExtraction.hs index 2cfb73ce..8b7e05de 100644 --- a/src-extra/transformation/JbeamEdit/Transformation/VertexExtraction.hs +++ b/src-extra/transformation/JbeamEdit/Transformation/VertexExtraction.hs @@ -117,13 +117,13 @@ breakVertices vertexPrefix allVertexNames ns = go [] ns allVertexNames Right ( vertexNames , reverse (maybeConsComment [node] assocPriorCmt) - , metaBefore ++ rest + , reverse metaBefore ++ rest ) else Right ( vertexNames , reverse (maybeConsComment currentTree assocPriorCmt) - , metaBefore ++ (node : rest) + , reverse metaBefore ++ (node : rest) ) | otherwise = go (node : acc) rest vertexNames where From 2e5ea1168ecfc0d841fe5b5a3427b6dad71fcf9d Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:58:02 +0200 Subject: [PATCH 6/8] Add a fixed-point spec over the transformed examples The existing idempotency spec runs one small regression fixture and compares names and Y positions only, so comments, metadata and beam references were never checked. That is how the comment reordering and the metadata shift stayed invisible. Parse each committed file in examples/transformed_jbeam/, transform it again and compare the whole formatted text. The committed file is already the output of one transform, so it is the input the second pass would see. Suspension is pending, because metadata is not carried across vertex tree boundaries. That is tracked separately. --- test-extra/transformation/Spec.hs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/test-extra/transformation/Spec.hs b/test-extra/transformation/Spec.hs index e7c00d8c..eddf37a1 100644 --- a/test-extra/transformation/Spec.hs +++ b/test-extra/transformation/Spec.hs @@ -2,7 +2,7 @@ module Spec ( main, ) where -import Data.List (isPrefixOf, isSuffixOf) +import Data.List (isInfixOf, isPrefixOf, isSuffixOf) import Data.Map qualified as M import Data.Set (Set) import Data.Set qualified as S @@ -50,6 +50,31 @@ topNodeSpec rs cfName tfConfig inFilename outFilename = do Right (formatNode rs node) describe desc . it "works" $ transformAndFormat `shouldBe` Right (T.pack output) +{- | Transforming an already transformed file must produce the same text +again. Unlike 'supportRenameIdempotencySpec' this compares the whole +formatted output, so it also covers comments, metadata and beam references. +-} +fixedPointSpec + :: RuleSet -> String -> TransformationConfig -> FilePath -> Spec +fixedPointSpec rs cfName tfConfig outFilename = do + output <- runIO $ readFile outFilename + let desc = + "with " + ++ cfName + ++ ": transforming " + ++ outFilename + ++ " again should leave it unchanged" + check = do + node <- parseJbeamFile outFilename + case transform M.empty tfConfig node of + Left err -> expectationFailure ("transform failed: " ++ T.unpack err) + Right (_, _, _, again) -> formatNode rs again `shouldBe` T.pack output + describe desc . it "works" $ + if "suspension" `isInfixOf` outFilename + then + pendingWith "metadata is not carried across vertex tree boundaries, issue #221" + else check + parseJbeamFile :: FilePath -> IO Node parseJbeamFile path = do let osPath = unsafeEncodeUtf path @@ -197,8 +222,12 @@ main = hspec $ do ++ cfName ++ ".jbeam" testInputFile cfName tfConfig' inFile = topNodeSpec (read rs) cfName tfConfig' inFile (outputFile cfName inFile) + testFixedPoint cfName tfConfig' inFile = + fixedPointSpec (read rs) cfName tfConfig' (outputFile cfName inFile) mapM_ (testInputFile "cfg-default" newTransformationConfig) inputFiles mapM_ (testInputFile "cfg-example" tfConfig) inputFiles + mapM_ (testFixedPoint "cfg-default" newTransformationConfig) inputFiles + mapM_ (testFixedPoint "cfg-example" tfConfig) inputFiles beamValidationSpec supportRenameIdempotencySpec letterEndingNodesSpec From 90260c8d0479861cc2746c92c0aa2864360b58ca Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Sun, 9 Aug 2026 21:43:41 +0200 Subject: [PATCH 7/8] Normalise line endings before parsing in the fixed-point spec A Windows checkout hands the parser CRLF, and commentStripSpace keeps the newline after a block comment opener by matching on "\n", so the comment collapsed and the texts differed. The spec is not asserting anything about line endings. --- test-extra/transformation/Spec.hs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/test-extra/transformation/Spec.hs b/test-extra/transformation/Spec.hs index eddf37a1..5e351d6d 100644 --- a/test-extra/transformation/Spec.hs +++ b/test-extra/transformation/Spec.hs @@ -2,12 +2,14 @@ module Spec ( main, ) where +import Data.ByteString.Lazy qualified as LBS import Data.List (isInfixOf, isPrefixOf, isSuffixOf) import Data.Map qualified as M import Data.Set (Set) import Data.Set qualified as S import Data.Text (Text) import Data.Text qualified as T +import Data.Text.Encoding (encodeUtf8) import Data.Vector qualified as V import GHC.IsList (fromList) import JbeamEdit.Core.Node (Node (..), NumberValue (..), expectArray) @@ -64,11 +66,21 @@ fixedPointSpec rs cfName tfConfig outFilename = do ++ ": transforming " ++ outFilename ++ " again should leave it unchanged" - check = do - node <- parseJbeamFile outFilename - case transform M.empty tfConfig node of - Left err -> expectationFailure ("transform failed: " ++ T.unpack err) - Right (_, _, _, again) -> formatNode rs again `shouldBe` T.pack output + -- Parse the same text the result is compared against, rather than + -- reading the file a second time, and drop the carriage returns a + -- Windows checkout leaves behind. formatNode always emits LF, so + -- the line endings the file happens to carry are not part of what + -- this spec is asserting. + expected = T.replace "\r\n" "\n" (T.pack output) + check = + case parseNodes (LBS.fromStrict (encodeUtf8 expected)) of + Left err -> + expectationFailure + ("failed to parse " ++ outFilename ++ ": " ++ T.unpack err) + Right node -> + case transform M.empty tfConfig node of + Left err -> expectationFailure ("transform failed: " ++ T.unpack err) + Right (_, _, _, again) -> formatNode rs again `shouldBe` expected describe desc . it "works" $ if "suspension" `isInfixOf` outFilename then From 93cdc465b077e24d5663937d94eeb2735920e77c Mon Sep 17 00:00:00 2001 From: webdevred <148627186+webdevred@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:07:26 +0200 Subject: [PATCH 8/8] Keep a trailing side letter that differs from the coordinate The letter is always appended again, so clipping only exists to avoid doubling it, and that only happens when the old letter equals the new one. Clipping unconditionally ate the rear group in rl_r48, which became rl_sm instead of rl_rsm. Frame and suspension output now match master byte for byte. --- .../transformed_jbeam/frame-cfg-default.jbeam | 32 +++++++++---------- .../transformed_jbeam/frame-cfg-example.jbeam | 32 +++++++++---------- .../JbeamEdit/Transformation.hs | 16 ++++++---- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/examples/transformed_jbeam/frame-cfg-default.jbeam b/examples/transformed_jbeam/frame-cfg-default.jbeam index 794c841c..ebee3b4b 100644 --- a/examples/transformed_jbeam/frame-cfg-default.jbeam +++ b/examples/transformed_jbeam/frame-cfg-default.jbeam @@ -93,7 +93,7 @@ // Support nodes ["rl_fsm", 0.053, -1.314, 0.382], // support for front - ["rl_sm", 0.053, 1.710, 0.565], // support for rear + ["rl_rsm", 0.053, 1.710, 0.565], // support for rear ["rlsm", 0.053, -0.024, 0.578], // support ], // --Beams-- @@ -320,21 +320,21 @@ // Rear end {"beamDeform" : 19000}, - ["rl_sm", "rl_m2"], - ["rl_sm", "rl_r7"], - ["rl_r4", "rl_sm"], - ["rl_sm", "rl_l7"], - ["rl_sm", "rl_l3"], - ["rl_sm", "rl_r5"], - ["rl_l6", "rl_sm"], - ["rl_m1", "rl_sm"], - ["rl_sm", "rl_r3"], - ["rl_l4", "rl_sm"], - ["rl_r6", "rl_sm"], - ["rl_m3", "rl_sm"], - ["rl_l2", "rl_sm"], - ["rl_sm", "rl_m0"], - ["rl_r2", "rl_sm"], + ["rl_rsm", "rl_m2"], + ["rl_rsm", "rl_r7"], + ["rl_r4", "rl_rsm"], + ["rl_rsm", "rl_l7"], + ["rl_rsm", "rl_l3"], + ["rl_rsm", "rl_r5"], + ["rl_l6", "rl_rsm"], + ["rl_m1", "rl_rsm"], + ["rl_rsm", "rl_r3"], + ["rl_l4", "rl_rsm"], + ["rl_r6", "rl_rsm"], + ["rl_m3", "rl_rsm"], + ["rl_l2", "rl_rsm"], + ["rl_rsm", "rl_m0"], + ["rl_r2", "rl_rsm"], ["rl_r4", "rl_l5"], // Front crush diff --git a/examples/transformed_jbeam/frame-cfg-example.jbeam b/examples/transformed_jbeam/frame-cfg-example.jbeam index 794c841c..ebee3b4b 100644 --- a/examples/transformed_jbeam/frame-cfg-example.jbeam +++ b/examples/transformed_jbeam/frame-cfg-example.jbeam @@ -93,7 +93,7 @@ // Support nodes ["rl_fsm", 0.053, -1.314, 0.382], // support for front - ["rl_sm", 0.053, 1.710, 0.565], // support for rear + ["rl_rsm", 0.053, 1.710, 0.565], // support for rear ["rlsm", 0.053, -0.024, 0.578], // support ], // --Beams-- @@ -320,21 +320,21 @@ // Rear end {"beamDeform" : 19000}, - ["rl_sm", "rl_m2"], - ["rl_sm", "rl_r7"], - ["rl_r4", "rl_sm"], - ["rl_sm", "rl_l7"], - ["rl_sm", "rl_l3"], - ["rl_sm", "rl_r5"], - ["rl_l6", "rl_sm"], - ["rl_m1", "rl_sm"], - ["rl_sm", "rl_r3"], - ["rl_l4", "rl_sm"], - ["rl_r6", "rl_sm"], - ["rl_m3", "rl_sm"], - ["rl_l2", "rl_sm"], - ["rl_sm", "rl_m0"], - ["rl_r2", "rl_sm"], + ["rl_rsm", "rl_m2"], + ["rl_rsm", "rl_r7"], + ["rl_r4", "rl_rsm"], + ["rl_rsm", "rl_l7"], + ["rl_rsm", "rl_l3"], + ["rl_rsm", "rl_r5"], + ["rl_l6", "rl_rsm"], + ["rl_m1", "rl_rsm"], + ["rl_rsm", "rl_r3"], + ["rl_l4", "rl_rsm"], + ["rl_r6", "rl_rsm"], + ["rl_m3", "rl_rsm"], + ["rl_l2", "rl_rsm"], + ["rl_rsm", "rl_m0"], + ["rl_r2", "rl_rsm"], ["rl_r4", "rl_l5"], // Front crush diff --git a/src-extra/transformation/JbeamEdit/Transformation.hs b/src-extra/transformation/JbeamEdit/Transformation.hs index f302f843..c1357484 100644 --- a/src-extra/transformation/JbeamEdit/Transformation.hs +++ b/src-extra/transformation/JbeamEdit/Transformation.hs @@ -369,12 +369,12 @@ renameVertexId treeType idx prefix = sideLetters :: String sideLetters = ['l', 'm', 'r'] -{- | Strip a trailing side letter and a trailing @s@ from a prefix. Feeding an -already renamed support vertex back in then lands on the same name: @rl_fsm@ -strips to @rl_f@ and is built back up to @rl_fsm@. +{- | Strip the @s@ and side letter a previous run appended. The side letter only +comes off when it is the one about to be put back, so @rl_r@ keeps its rear @r@ +while @bfl@ loses the @l@ that would otherwise be doubled. -} -dropSupportSuffix :: Text -> Text -dropSupportSuffix prefix = +dropSupportSuffix :: Text -> Text -> Text +dropSupportSuffix sideLetter prefix = bool withoutSide (T.init withoutSide) @@ -384,7 +384,7 @@ dropSupportSuffix prefix = bool prefix (T.init prefix) - (T.length prefix > 2 && T.last prefix `elem` sideLetters) + (T.length prefix > 2 && T.takeEnd 1 prefix == sideLetter) {- | The name a vertex gets, before the running index is appended. Support prefixes are a fixed point of this, which is what lets it double as a sort @@ -394,7 +394,9 @@ vertexPrefix :: UpdateNamesMap -> XGroupBreakpoints -> VertexTreeType -> Vertex -> Text vertexPrefix newNames brks treeType v | treeType == SupportTree = - updatedPrefix (dropSupportSuffix prefix) <> T.singleton 's' <> typeSpecific + updatedPrefix (dropSupportSuffix typeSpecific prefix) + <> T.singleton 's' + <> typeSpecific | T.length prefix >= 3 && T.last prefix' == 's' = updatedPrefix (T.init prefix') <> typeSpecific