import FileSheet, {
FILE_SHEET_FIELD_LABEL_CLASSES,
FILE_SHEET_PRECISION_SLIDER_CLASSES,
FileSheetSection,
FileSheetSliderField,
FileSheetSubsection,
FileSheetToggleRow,
parseFileSheetNumberInput
} from "./FileSheet";
import FileMetadataSection from "./FileMetadataSection";
import {
Accordion
} from "../ui/accordion";
import { Badge } from "../ui/badge";
import { Slider } from "../ui/slider";
import {
DEFAULT_GCODE_PREVIEW_DETAIL_LEVEL,
GCODE_PREVIEW_DETAIL_MAX,
GCODE_PREVIEW_DETAIL_MIN
} from "cadjs/lib/gcode/buildPreviewMesh";
import FileStatusSection from "./FileStatusSection";
const fieldLabelClasses = FILE_SHEET_FIELD_LABEL_CLASSES;
const compactNumberBadgeClasses = "h-4 rounded-sm px-1.5 py-0 text-[10px] font-medium leading-none";
function formatNumber(value, digits = 2) {
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue.toFixed(digits) : (0).toFixed(digits);
}
function formatCount(value) {
const numericValue = Number(value);
return Number.isFinite(numericValue) ? String(Math.round(numericValue)) : "0";
}
function formatOrdinal(value) {
const numericValue = Math.round(Number(value) || 0);
const absoluteValue = Math.abs(numericValue);
const tensRemainder = absoluteValue % 100;
if (tensRemainder >= 11 && tensRemainder <= 13) {
return `${numericValue}th`;
}
switch (absoluteValue % 10) {
case 1:
return `${numericValue}st`;
case 2:
return `${numericValue}nd`;
case 3:
return `${numericValue}rd`;
default:
return `${numericValue}th`;
}
}
function formatDistance(value, digits = 1) {
return `${formatNumber(value, digits)} mm`;
}
function formatCommandCount(value) {
const count = formatCount(value);
return `${count} cmd${count === "1" ? "" : "s"}`;
}
function formatFeatureCategory(value) {
const normalized = String(value || "").trim();
return normalized ? `${normalized[0].toUpperCase()}${normalized.slice(1)}` : "Other";
}
function previewDetailLabel(value) {
switch (Math.round(Number(value) || DEFAULT_GCODE_PREVIEW_DETAIL_LEVEL)) {
case 1:
return "Light";
case 2:
return "Lean";
case 4:
return "Dense";
case 5:
return "High";
case 6:
return "Very high";
case 7:
return "Max adaptive";
case 3:
default:
return "Standard";
}
}
function normalizedBounds(bounds) {
const min = Array.isArray(bounds?.min) ? bounds.min : [0, 0, 0];
const max = Array.isArray(bounds?.max) ? bounds.max : [0, 0, 0];
return { min, max };
}
function boundsAxisText(bounds, axis, digits = 1) {
const { min, max } = normalizedBounds(bounds);
return `${formatNumber(min[axis], digits)} to ${formatNumber(max[axis], digits)} mm`;
}
function visibleLayerText(layerCount, maxLayer) {
if (layerCount < 1) {
return "0 / 0";
}
const visibleCount = maxLayer + 1;
return visibleCount > 1
? `1-${visibleCount} / ${layerCount}`
: `1 / ${layerCount}`;
}
function parseVisibleLayerInput(value, fallbackVisibleCount, layerCount) {
if (layerCount < 1) {
return 0;
}
const text = String(value ?? "").trim();
const rangeMatch = text.match(/\b\d+\s*-\s*(\d+)\b/);
const parsedValue = rangeMatch?.[1] ?? value;
return parseFileSheetNumberInput(parsedValue, {
fallback: fallbackVisibleCount,
min: 1,
max: Math.max(layerCount, 0),
integer: true
}) - 1;
}
function GcodeValueField({ label, value, mono = false }) {
const displayValue = String(value ?? "");
return (
);
}
export default function GcodeFileSheet({
open,
isDesktop,
width,
selectedEntry = null,
onOpenChange,
onStartResize,
gcodeData = null,
previewMetadata = null,
maxLayer = 0,
showTravel = false,
fullDetail = false,
previewDetailLevel = DEFAULT_GCODE_PREVIEW_DETAIL_LEVEL,
onMaxLayerChange,
onShowTravelChange,
onFullDetailChange,
onPreviewDetailLevelChange,
fileDownloadAvailable = false,
viewerServerInfo = null,
localFileOpenAvailable = false,
fileAccessBusyKey = "",
onOpenFileAsset,
suppressDynamicMetadataStatus = false,
statusItems = [],
themeSections = null,
openSectionIds = [],
onOpenSectionIdsChange
}) {
const layers = Array.isArray(gcodeData?.layers) ? gcodeData.layers : [];
const stats = gcodeData?.stats || {};
const features = Array.isArray(gcodeData?.features) ? gcodeData.features : [];
const supportFeatures = features.filter((feature) => feature?.category === "support");
const hasSupports = supportFeatures.some((feature) => (
Number(feature?.extrusionMoves) > 0 ||
Number(feature?.markerCount) > 0
));
const featureMarkerCount = features.reduce((total, feature) => total + (Number(feature?.markerCount) || 0), 0);
const renderedSegments = Number(previewMetadata?.renderedSegments);
const visibleSegments = Number(previewMetadata?.visibleSegments);
const layerStride = Number(previewMetadata?.layerStride);
const renderedLayers = Number(previewMetadata?.renderedLayers);
const visiblePreviewLayers = Number(previewMetadata?.visibleLayers);
const decimatedPreview = Boolean(previewMetadata?.decimated);
const renderedPathText = Number.isFinite(renderedSegments) && Number.isFinite(visibleSegments)
? `${formatCount(renderedSegments)} / ${formatCount(visibleSegments)}`
: "";
const renderedLayerText = Number.isFinite(renderedLayers) && Number.isFinite(visiblePreviewLayers)
? `${formatCount(renderedLayers)} / ${formatCount(visiblePreviewLayers)}`
: "";
const layerCount = layers.length;
const safePreviewDetailLevel = Math.min(
Math.max(Math.round(Number(previewDetailLevel) || DEFAULT_GCODE_PREVIEW_DETAIL_LEVEL), GCODE_PREVIEW_DETAIL_MIN),
GCODE_PREVIEW_DETAIL_MAX
);
const safeMaxLayer = layerCount > 0
? Math.min(Math.max(Math.trunc(Number(maxLayer) || 0), 0), layerCount - 1)
: 0;
const hasGcodeData = Boolean(gcodeData);
const visibleLayers = visibleLayerText(layerCount, safeMaxLayer);
return (
{!hasGcodeData ? (
Loading G-code...
) : null}
{formatCount(layerCount)} layer{layerCount === 1 ? "" : "s"}
{formatCount(stats.extrusionMoves)} path{formatCount(stats.extrusionMoves) === "1" ? "" : "s"}
Supports {hasSupports ? "yes" : "no"}
{decimatedPreview ? (
Layer preview
) : null}
{
onMaxLayerChange?.(parseVisibleLayerInput(nextValue, safeMaxLayer + 1, layerCount));
}}
valueInputProps={{
disabled: layerCount <= 1,
ariaLabel: "Visible G-code layers value",
inputMode: "numeric"
}}
>
{
onMaxLayerChange?.(Number(nextValue?.[0] ?? 0));
}}
className={FILE_SHEET_PRECISION_SLIDER_CLASSES}
aria-label="Visible G-code layers"
/>
{
onPreviewDetailLevelChange?.(parseFileSheetNumberInput(nextValue, {
fallback: safePreviewDetailLevel,
min: GCODE_PREVIEW_DETAIL_MIN,
max: GCODE_PREVIEW_DETAIL_MAX,
integer: true
}));
}}
valueInputProps={{
disabled: fullDetail,
ariaLabel: "G-code preview detail value",
inputMode: "numeric"
}}
>
{
onPreviewDetailLevelChange?.(Number(nextValue?.[0] ?? DEFAULT_GCODE_PREVIEW_DETAIL_LEVEL));
}}
className={FILE_SHEET_PRECISION_SLIDER_CLASSES}
aria-label="G-code preview detail"
/>
onShowTravelChange?.(checked)}
ariaLabel="Show G-code travel moves"
/>
onFullDetailChange?.(checked)}
ariaLabel="Render full G-code detail"
/>
Features
{features.length ? (
{features.length}
) : null}
{hasSupports ? (
Supports
) : null}
)}
>
{!features.length ? (
No slicer feature annotations found. The preview can still show layers, extrusion paths, and travel moves.
) : (
<>
Supports {hasSupports ? "included" : "not detected"}
{formatCount(featureMarkerCount)} markers
>
)}
{themeSections}
);
}