mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 06:09:56 -05:00
fix: stl plate preview thumbnails + add them to downloads modal
This commit is contained in:
1 parent
2813d47049
commit
56f842ed6f
4 files changed
+63
-14
No files matched your search
@@ -3,6 +3,7 @@ import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { printsApi } from "../../../api/prints";
|
||||
import { type ResolvedTheme } from "../../../constants/settingsOptions";
|
||||
@@ -15,8 +16,17 @@ type ModelSnapshotProps = {
|
||||
plateId?: string;
|
||||
theme: ResolvedTheme;
|
||||
mode?: "automatic" | "on-demand";
|
||||
/** For tiny slots (e.g. a plate list's 32px thumbnail): no status text or buttons, just the
|
||||
* image once ready (a small spinner while generating). Also renders at a fixed card-sized
|
||||
* resolution instead of the slot's own size -- the result is persisted as the plate's
|
||||
* thumbnail (see uploadGeneratedThumbnail) and reused by full-size grid cards. */
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
// Render size for compact snapshots: the same 4:3 as a grid card, large enough to stay sharp there.
|
||||
const COMPACT_RENDER_WIDTH = 480;
|
||||
const COMPACT_RENDER_HEIGHT = 360;
|
||||
|
||||
// Every ModelSnapshot on the page shares one job queue (see queueSnapshotJob), so a single model
|
||||
// that hangs mid-load (bad network response, pathological geometry) would otherwise wedge every
|
||||
// other card's preview behind it forever. This bounds each job so the queue always keeps moving.
|
||||
@@ -38,7 +48,7 @@ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
});
|
||||
}
|
||||
|
||||
export function ModelSnapshot({ url, ext, plateId, mode = "automatic" }: ModelSnapshotProps) {
|
||||
export function ModelSnapshot({ url, ext, plateId, mode = "automatic", compact = false }: ModelSnapshotProps) {
|
||||
const { t } = useTranslation(["library", "common"]);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [snapshot, setSnapshot] = useState<string | null>(null);
|
||||
@@ -63,8 +73,8 @@ export function ModelSnapshot({ url, ext, plateId, mode = "automatic" }: ModelSn
|
||||
setState("loading");
|
||||
try {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const width = Math.max(120, Math.floor(rect.width || 240));
|
||||
const height = Math.max(120, Math.floor(rect.height || 180));
|
||||
const width = compact ? COMPACT_RENDER_WIDTH : Math.max(120, Math.floor(rect.width || 240));
|
||||
const height = compact ? COMPACT_RENDER_HEIGHT : Math.max(120, Math.floor(rect.height || 180));
|
||||
const image = await queueSnapshotJob(async () => {
|
||||
if (disposed) return null;
|
||||
return withTimeout(generateModelSnapshot(url, ext, width, height, "light"), SNAPSHOT_TIMEOUT_MS);
|
||||
@@ -109,7 +119,7 @@ export function ModelSnapshot({ url, ext, plateId, mode = "automatic" }: ModelSn
|
||||
// retryToken isn't read in the effect body -- it's a bump counter whose only job is to
|
||||
// force this effect to re-run when the user clicks Retry.
|
||||
// oxlint-disable-next-line react/exhaustive-effect-dependencies
|
||||
}, [url, ext, plateId, mode, requested, retryToken]);
|
||||
}, [url, ext, plateId, mode, requested, retryToken, compact]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -132,6 +142,8 @@ export function ModelSnapshot({ url, ext, plateId, mode = "automatic" }: ModelSn
|
||||
alt={t("library:modelViewer.previewAlt")}
|
||||
sx={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
/>
|
||||
) : compact ? (
|
||||
state === "loading" && <CircularProgress size={14} />
|
||||
) : state === "loading" ? (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t("library:modelViewer.generatingPreview")}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { type Plate, printsApi } from "../../api/prints";
|
||||
import { MODEL_EXTS } from "../../constants/fileTypes";
|
||||
import { extOf } from "../../utils/fileExtensions";
|
||||
import { ModelSnapshot } from "./ModelViewer/ModelSnapshot";
|
||||
|
||||
type Props = {
|
||||
plate: Plate;
|
||||
/** Square edge in px. */
|
||||
size?: number;
|
||||
};
|
||||
|
||||
/** A small square preview of one plate for plate lists (3D preview switcher, download picker):
|
||||
* its stored thumbnail when it has one; otherwise, for a 3D file (STL/OBJ/STEP carry no
|
||||
* embedded thumbnail, and only a model's first plate gets one from its grid card), a snapshot
|
||||
* rendered on the spot -- which ModelSnapshot also saves server-side, so it's there next time;
|
||||
* otherwise a plain placeholder. */
|
||||
export default function PlateThumbnail({ plate, size = 32 }: Props) {
|
||||
const boxSx = { width: size, height: size, flexShrink: 0, borderRadius: 0.75, overflow: "hidden" };
|
||||
const ext = extOf(plate.filename);
|
||||
|
||||
if (plate.thumb_url) {
|
||||
return (
|
||||
<Box
|
||||
component="img"
|
||||
src={printsApi.fileUrl(plate.thumb_url)}
|
||||
alt={plate.filename}
|
||||
sx={{ ...boxSx, objectFit: "cover" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (MODEL_EXTS.has(ext)) {
|
||||
return (
|
||||
<Box sx={boxSx}>
|
||||
<ModelSnapshot url={printsApi.fileUrl(plate.url)} ext={ext} plateId={plate.id} theme="light" compact />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return <Box sx={{ ...boxSx, bgcolor: "action.hover" }} />;
|
||||
}
|
||||
@@ -8,10 +8,12 @@ import Stack from "@mui/material/Stack";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import List from "@mui/material/List";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import type { Plate } from "../../api/prints";
|
||||
import PlateThumbnail from "../../components/media/PlateThumbnail";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -44,6 +46,9 @@ export default function DownloadPickerDialog({ open, onClose, downloading, sorte
|
||||
<List disablePadding>
|
||||
{sortedPlates.map((plate, idx) => (
|
||||
<ListItemButton key={plate.id} onClick={() => downloadPlate(plate)} disabled={downloading} sx={{ borderRadius: 1 }}>
|
||||
<ListItemIcon sx={{ minWidth: 60 }}>
|
||||
<PlateThumbnail plate={plate} size={48} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t("models:detail.plateLabel", { n: idx + 1 })}
|
||||
secondary={plate.filename}
|
||||
|
||||
@@ -15,6 +15,7 @@ import CloseIcon from "@mui/icons-material/Close";
|
||||
import { type Print, printsApi } from "../../api/prints";
|
||||
import { MODEL_EXTS } from "../../constants/fileTypes";
|
||||
import { extOf } from "../../utils/fileExtensions";
|
||||
import PlateThumbnail from "../../components/media/PlateThumbnail";
|
||||
import ModelViewer, { type CameraView, type ModelViewerHandle, type RenderStyle } from "../../components/media/ModelViewer";
|
||||
import type { PlateSummary } from "../../utils/bambuThreeMf";
|
||||
import PreviewToolbar, { DEFAULT_PREVIEW_COLOR } from "./PreviewToolbar";
|
||||
@@ -137,16 +138,7 @@ export default function Model3DPreviewModal({ print, onClose }: Props) {
|
||||
sx={{ borderRadius: 1, mb: 0.5 }}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 40 }}>
|
||||
{plate.thumb_url ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={printsApi.fileUrl(plate.thumb_url)}
|
||||
alt={plate.filename}
|
||||
sx={{ width: 32, height: 32, borderRadius: 0.75, objectFit: "cover" }}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{ width: 32, height: 32, borderRadius: 0.75, bgcolor: "action.hover" }} />
|
||||
)}
|
||||
<PlateThumbnail plate={plate} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t("models:detail.plateLabel", { n: idx + 1 })}
|
||||
|
||||
Reference in new issue
Block a user