mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 22:22:24 -05:00
fix: harden mesh check against bad indices and tidy review follow-ups
- Reject triangle indices outside the vertex list instead of stalling the queue - Leave degenerate triangles out of body bounding boxes - Skip the quality fetch without a print id and never show a previous print's result - Disable the per-check switches while a save is in flight
This commit is contained in:
1 parent
f913710cc4
commit
8b4653e415
5 files changed
+28
-7
No files matched your search
@@ -112,11 +112,17 @@ export function analyzeMesh(vertices: ArrayLike<number>, triangles?: ArrayLike<n
|
||||
const firstVertex = new Uint32Array(vertexCount);
|
||||
for (let v = ids.length - 1; v >= 0; v--) firstVertex[ids[v]] = v;
|
||||
const position = (id: number, axis: number) => vertices[firstVertex[id] * 3 + axis];
|
||||
// Degenerate faces join no body, so they mustn't make a bounding box of their own either.
|
||||
const skipped = new Uint8Array(triangleCount);
|
||||
|
||||
for (let t = 0; t < triangleCount; t++) {
|
||||
const a = corner(t * 3);
|
||||
const b = corner(t * 3 + 1);
|
||||
const c = corner(t * 3 + 2);
|
||||
// An index past the vertex list would make NaN edge keys, which never group and stall the loop below.
|
||||
if (a === undefined || b === undefined || c === undefined) {
|
||||
throw new RangeError("Triangle index outside the vertex list");
|
||||
}
|
||||
const ax = position(a, 0);
|
||||
const ay = position(a, 1);
|
||||
const az = position(a, 2);
|
||||
@@ -131,6 +137,7 @@ export function analyzeMesh(vertices: ArrayLike<number>, triangles?: ArrayLike<n
|
||||
const nz = ux * vy - uy * vx;
|
||||
if (a === b || b === c || a === c || nx * nx + ny * ny + nz * nz <= degenerateArea * degenerateArea) {
|
||||
degenerate++;
|
||||
skipped[t] = 1;
|
||||
continue;
|
||||
}
|
||||
// The tetrahedron to the origin; the sum over a closed surface is its volume.
|
||||
@@ -172,6 +179,7 @@ export function analyzeMesh(vertices: ArrayLike<number>, triangles?: ArrayLike<n
|
||||
// Each body's bounding box, keyed by its root vertex.
|
||||
const boxes = new Map<number, number[]>();
|
||||
for (let t = 0; t < triangleCount; t++) {
|
||||
if (skipped[t]) continue;
|
||||
for (let k = 0; k < 3; k++) {
|
||||
const id = corner(t * 3 + k);
|
||||
const root = findRoot(parent, id);
|
||||
|
||||
@@ -75,7 +75,11 @@ describe("analyzeMesh", () => {
|
||||
it("counts degenerate triangles without treating them as holes", () => {
|
||||
const sliver = [0, 0, 0, 5, 0, 0, 10, 0, 0];
|
||||
const stats = analyzeMesh(soup([...box(), sliver]));
|
||||
expect(stats).toMatchObject({ degenerateTriangles: 1, openEdges: 0 });
|
||||
expect(stats).toMatchObject({ degenerateTriangles: 1, openEdges: 0, shells: 1, fragments: 0 });
|
||||
});
|
||||
|
||||
it("rejects a triangle index outside the vertex list", () => {
|
||||
expect(() => analyzeMesh([0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 3])).toThrow(RangeError);
|
||||
});
|
||||
|
||||
it("tells a stray fragment from a second part", () => {
|
||||
|
||||
@@ -14,8 +14,9 @@ export type QualityOutcome = "good" | "issues" | null;
|
||||
export function useQualityCheck(printId: string, onUnauthorized?: () => void) {
|
||||
const { t } = useTranslation("models");
|
||||
const showToast = useToast();
|
||||
const [quality, setQuality] = useState<PrintQuality | null>(null);
|
||||
const [outcome, setOutcome] = useState<QualityOutcome>(null);
|
||||
// Tagged with their print, so a newly opened model never shows the last one's result for a render.
|
||||
const [quality, setQuality] = useState<{ printId: string; value: PrintQuality } | null>(null);
|
||||
const [outcome, setOutcome] = useState<{ printId: string; value: QualityOutcome } | null>(null);
|
||||
// Kept out of the effect's deps: a new toast function or translation mustn't restart a running check.
|
||||
const notify = useRef({ showToast, t, onUnauthorized });
|
||||
useEffect(() => {
|
||||
@@ -23,19 +24,20 @@ export function useQualityCheck(printId: string, onUnauthorized?: () => void) {
|
||||
}, [showToast, t, onUnauthorized]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!printId) return;
|
||||
let cancelled = false;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
setQuality(null);
|
||||
setOutcome(null);
|
||||
|
||||
const settle = (next: PrintQuality, watched: boolean) => {
|
||||
setQuality(next);
|
||||
setQuality({ printId, value: next });
|
||||
if (next.status === "checking") {
|
||||
timer = setTimeout(() => void poll(), POLL_MS);
|
||||
return;
|
||||
}
|
||||
if (!watched || (next.status !== "good" && next.status !== "issues")) return;
|
||||
setOutcome(next.status);
|
||||
setOutcome({ printId, value: next.status });
|
||||
if (next.status === "issues") {
|
||||
// Only minor problems, which slicers usually cope with: a note rather than a warning.
|
||||
const minor = worstSeverity(next) === "minor";
|
||||
@@ -78,5 +80,8 @@ export function useQualityCheck(printId: string, onUnauthorized?: () => void) {
|
||||
};
|
||||
}, [printId]);
|
||||
|
||||
return { quality, outcome };
|
||||
return {
|
||||
quality: quality?.printId === printId ? quality.value : null,
|
||||
outcome: outcome?.printId === printId ? outcome.value : null,
|
||||
};
|
||||
}
|
||||
@@ -18,10 +18,12 @@ type Props = {
|
||||
checksOff: QualityCheckKey[];
|
||||
onChange: (checksOff: QualityCheckKey[]) => void;
|
||||
onClose: () => void;
|
||||
/** While a save is in flight, so overlapping saves can't land out of order. */
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
/** One switch per kind of print problem. Each saves as soon as it's switched. */
|
||||
export default function QualityChecksDialog({ open, checksOff, onChange, onClose }: Props) {
|
||||
export default function QualityChecksDialog({ open, checksOff, onChange, onClose, disabled }: Props) {
|
||||
const { t } = useTranslation(["app", "common"]);
|
||||
const toggle = (key: QualityCheckKey, on: boolean) =>
|
||||
onChange(on ? checksOff.filter((off) => off !== key) : [...checksOff, key]);
|
||||
@@ -52,6 +54,7 @@ export default function QualityChecksDialog({ open, checksOff, onChange, onClose
|
||||
</Box>
|
||||
<Switch
|
||||
checked={!checksOff.includes(key)}
|
||||
disabled={disabled}
|
||||
onChange={(e) => toggle(key, e.target.checked)}
|
||||
slotProps={{ input: { "aria-labelledby": titleId } }}
|
||||
/>
|
||||
|
||||
@@ -149,6 +149,7 @@ export default function QualityChecksSection({ onUnauthorized }: Props) {
|
||||
<QualityChecksDialog
|
||||
open={checksOpen}
|
||||
checksOff={settings.checks_off}
|
||||
disabled={saving}
|
||||
onChange={(checksOff) => void save({ checks_off: checksOff })}
|
||||
onClose={() => setChecksOpen(false)}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user