pad: the Light/Custom pad's top grid is wound like its tip fans, side wall and bottom -- it ran the other way (closed, but the top inside-out against the rest), Bambu read the pad as a broken mesh and refused the separate-object 3MF over a G-code path conflict with the part; PrusaSlicer warned. Same triangles, only the winding: walls byte-identical over the 54 goldens (hashes re-recorded), sweep metrics unchanged. tests: isOriented (isClosed only counts edges), every pad style at X20/45/60

This commit is contained in:
matthewtrahan authored and Matthew Trahan committed 2026-10-07 11:10:38 -05:00
1 parent 3d4f88cfd9
commit a84098c7da
23 files changed
+59 -22

No files matched your search

+21
View File
@@ -115,6 +115,27 @@ export function isClosed(tris) {
return true;
}
/** Every edge is walked once each way (no face flipped against its neighbour) and
* the enclosed volume is positive (normals point out). isClosed only counts edges,
* so it passes a mesh with a patch wound inside-out -- one a slicer misreads. */
export function isOriented(tris) {
const key = (p) => `${Math.round(p[0] * 1e3)},${Math.round(p[1] * 1e3)},${Math.round(p[2] * 1e3)}`;
const edges = new Map();
let vol = 0;
for (let i = 0; i < tris.length; i += 3) {
const [p, q, r] = [tris[i], tris[i + 1], tris[i + 2]];
vol += p[0] * (q[1] * r[2] - q[2] * r[1]) - p[1] * (q[0] * r[2] - q[2] * r[0]) + p[2] * (q[0] * r[1] - q[1] * r[0]);
for (let e = 0; e < 3; e++) {
const a = key(tris[i + e]), b = key(tris[i + (e + 1) % 3]);
if (a === b) continue;
const k = a < b ? `${a}|${b}` : `${b}|${a}`;
edges.set(k, (edges.get(k) ?? 0) + (a < b ? 1 : -1));
}
}
for (const c of edges.values()) if (c !== 0) return false;
return vol > 0;
}
/** Squared distance from point p to triangle (a,b,c) (each [x,y,z]). */
export function ptTriDist2(p, a, b, c) {
const sub = (u, v) => [u[0] - v[0], u[1] - v[1], u[2] - v[2]];
+1 -1
View File
@@ -63,5 +63,5 @@
]
}
],
"sha256": "e876bede51197e3ff22204926e5551ac72da548b46f4bba2d9fd5c9e1e3c197f"
"sha256": "b66ad252dbae0921ce6bdf285c9875c97d2a5efff386a635e85a879fff0ea40c"
}
+1 -1
View File
@@ -63,5 +63,5 @@
]
}
],
"sha256": "7ddf37cdeeb205df088ff6a576096249ef170860689aadbb2780512415e06ed5"
"sha256": "dfb572b337c7b42a88887ef2989f6be2c3d769f314a76ebb68f28ea8ab71b2cd"
}
+1 -1
View File
@@ -63,5 +63,5 @@
]
}
],
"sha256": "f7efb66fb90b39aa0d4346f1666a0c337a85fd8b7b5e549075c1f869e7233c6b"
"sha256": "90ac5b66684b22a6dbfc57aad4aeb7e8aab9223cf8610ef3d9338d1cb49c24d4"
}
+1 -1
View File
@@ -31,5 +31,5 @@
"length": 30
}
],
"sha256": "9754230987afc241117ef27ec2a844bc1f732ed799948ba9fad7f2dbfb1f566f"
"sha256": "e732369394923e9902d92ff24aead0644806adc4b1994eccd7d6505a11b010a4"
}
+1 -1
View File
@@ -66,5 +66,5 @@
]
}
],
"sha256": "7acaf466365c036d868f72eeade0146f793985fe6bb1bfe34a093eaeb60177e9"
"sha256": "61f3c93c411482f26ef212fdbc88b99a3c9140a07406e7cdd6cc726e4ab8052a"
}
+1 -1
View File
@@ -79,5 +79,5 @@
]
}
],
"sha256": "fd21d177b088386a1389cafe665d48706ec7e9102a439bd673f7b539e306d60c"
"sha256": "5b71f3e92b38628681a4d6aefb99414b61b004e495a8e90299c67de3ab0de191"
}
+1 -1
View File
@@ -63,5 +63,5 @@
]
}
],
"sha256": "18ff8f72b1de8331938d4eb8df184fb02de98b1541505eedd48794f55fc11de7"
"sha256": "1b0884b96f061b179102cdf0cfe425adbf2271aa7d2c9f21a271681729e425de"
}
+1 -1
View File
@@ -79,5 +79,5 @@
]
}
],
"sha256": "016e8fa0c302dc4f9f7b2982aca6569f4b526f61ce6fe5d8a2df5f21d5777bcb"
"sha256": "bba1ffcb675e190c42d7611db1edcc800ebdf869c41949dbfd1e8dbb99584d3d"
}
+1 -1
View File
@@ -31,5 +31,5 @@
]
}
],
"sha256": "0f1947d5432c9b86a5034b2594c8b4214cbaca5cf9db48ed9601d928f67a81de"
"sha256": "c6d6c967efcf271c4743a4cae8ef66adc522591f7d61ecded6ea492f9490e06f"
}
+1 -1
View File
@@ -63,5 +63,5 @@
]
}
],
"sha256": "f7efb66fb90b39aa0d4346f1666a0c337a85fd8b7b5e549075c1f869e7233c6b"
"sha256": "90ac5b66684b22a6dbfc57aad4aeb7e8aab9223cf8610ef3d9338d1cb49c24d4"
}
+1 -1
View File
@@ -63,5 +63,5 @@
]
}
],
"sha256": "8aa787df16c41675897a2c3bebceb607fdceae0655bcb19b67ef8d1bef336794"
"sha256": "842f25edf4ae90cf557687686ddbc9e293f8ea29cea9ddce9dac455f7baa62c5"
}
+1 -1
View File
@@ -31,5 +31,5 @@
]
}
],
"sha256": "15536c40b46aaf6b566b754ff9bc8c2c59373b93cbf48fa77608dd454a382a3c"
"sha256": "5eee6aeb5f5e54fb449363b0cb14a1341b65f7f7f181d2bfd5adfe9c0ef375d2"
}
+1 -1
View File
@@ -127,5 +127,5 @@
]
}
],
"sha256": "277859cb967ee2e3e6f00fd6030caf60b0d4e114555faa22105f94772a0eaa52"
"sha256": "a7ee3a69557dc0a21679f269751e0a68dfe798f7a72f0bf69be9df8052684931"
}
+1 -1
View File
@@ -79,5 +79,5 @@
]
}
],
"sha256": "c0aeea1f027a208def2b061e5dfd0e3ea086cd1052155f1df095fa9f8c4a742e"
"sha256": "5ea5698aa85944d1dfcd75b543ad95fa37f7ff7516903724910f32c6af126120"
}
+1 -1
View File
@@ -47,5 +47,5 @@
]
}
],
"sha256": "2405663113977690550e899839e0df53371da71879c676bbcbcf64e293b381f8"
"sha256": "304b2cdf31f6532b4fcb655f5cd69cf0c5ed4bfc8949b687ff62e956cd5c6229"
}
+1 -1
View File
@@ -111,5 +111,5 @@
]
}
],
"sha256": "c6ed2f8d0f49f5f84abfa4a054de21f6827b956d49c4218766b82f6806820756"
"sha256": "994920221d1211092e90563ab08fb20471ee59ec97876d6b6abc5d7aa940b72c"
}
+1 -1
View File
@@ -63,5 +63,5 @@
]
}
],
"sha256": "18ff8f72b1de8331938d4eb8df184fb02de98b1541505eedd48794f55fc11de7"
"sha256": "1b0884b96f061b179102cdf0cfe425adbf2271aa7d2c9f21a271681729e425de"
}
+1 -1
View File
@@ -95,5 +95,5 @@
]
}
],
"sha256": "843aa33f69e6682d8c117b6792e5588f21ab13d9c6c264dc762f8ec0c1d6b240"
"sha256": "3b14893331c7a0c1f54c632e9fa770c71f48acdb8aba7f58a57dc60753663e59"
}
+1 -1
View File
@@ -47,5 +47,5 @@
]
}
],
"sha256": "285058654c6fdca843eb14a52289e970a1c92b18024b5016313cf9345de33d06"
"sha256": "3b452ca3d716d483dcca675f59fe3ae9350baf56d3105fb88f40ac7b4544c804"
}
+1 -1
View File
@@ -63,5 +63,5 @@
]
}
],
"sha256": "b14e009ea9057f88a7d0c29c9fa445152313c2406316da039128e5d7e5582a39"
"sha256": "61b28cb99c2faefb46b9725c871e1038f67d194a9d2257638a4dd296de0c5c26"
}
+14 -1
View File
@@ -3,7 +3,7 @@
// part's flank -- and stay watertight. These pin all three so a revert to the grid
// (or a broken mesh) fails loudly.
import { loadModel, analyze, fins, isClosed, rotX, assert } from './_util.js';
import { loadModel, analyze, fins, isClosed, isOriented, rotX, assert } from './_util.js';
// Build with a pad style set for this call only (PAD.style is module state).
function withStyle(style, fn) {
@@ -145,6 +145,19 @@ Deno.test('pad (brim): one layer thick, whatever the layer height', () => {
}
});
// Every pad style is one consistently wound outward solid. The Light/Custom top grid
// once ran the other way to its tip fans, side wall and bottom: closed, but with
// the top inside-out against the rest. Bambu misread it and refused the
// separate-object 3MF ("G-code path conflicts" with the part); PrusaSlicer warned.
Deno.test('pad: every style is wound consistently outward, at any tilt', () => {
for (const style of ['light', 'sure', 'custom']) for (const deg of [20, 45, 60]) {
const topo = loadModel('cube'), rot = rotX(deg);
const b = withStyle(style, () => fins.buildFins(topo, analyze(topo, 45, rot), rot, { mode: 'auto', bedPad: true, tines: true }));
assert(b.padTriangles.length > 0, `${style} X${deg}: no pad`);
assert(isOriented(b.padTriangles), `${style} X${deg}: pad has faces wound against their neighbours`);
}
});
// What the SLICER sees: the pad's and the part's sections at the first layer's
// mid-height. PrusaSlicer / Orca / Bambu merge any gap under 2 x 0.049mm, so the
// sliced gap must clear 0.098 -- at every tilt, not just 45deg: 0.1mm through a
+4 -1
View File
@@ -361,7 +361,10 @@ function brimPad(partTris, contact, e, h, g, grab, layerH, outline, out) {
}
for (let i = 0; i < cols.length - 1; i++) {
const A = cols[i], B = cols[i + 1];
for (let j = 0; j < nT - 1; j++) { tri(A[j], B[j], B[j + 1]); tri(A[j], B[j + 1], A[j + 1]); }
// wound like the tip fans, side wall and bottom (it once ran the other way: the
// top was inside-out against the rest, and Bambu, reading a broken pad, refused
// the separate-object 3MF over a G-code path conflict with the part)
for (let j = 0; j < nT - 1; j++) { tri(A[j], B[j + 1], B[j]); tri(A[j], A[j + 1], B[j + 1]); }
}
// boundary ring (tipL, the +t rim left to right, tipR, the -t rim right to
// left), then its side wall and the flat bottom fan