From 64b21079725bda03270a1275907db284a6ff05f1 Mon Sep 17 00:00:00 2001
From: Brice Johnson <1939015+Bjohnson131@users.noreply.github.com>
Date: Fri, 10 Jul 2026 23:03:41 -0500
Subject: [PATCH] Add multi-head printing support (squashed)
Squashed the mph-calc-develop feature branch prior to rebasing onto
upstream/develop.
Signed-off-by: Brice Johnson <1939015+Bjohnson131@users.noreply.github.com>
---
src/App.tsx | 73 +-
src/assets/diagrams/01_window_run_diagram.svg | 72 ++
.../diagrams/02_nozzle_swap_schedule.svg | 117 ++
.../diagrams/03_beer_lambert_blending.svg | 51 +
src/assets/diagrams/05_combo_search_space.svg | 180 +++
src/components/AutoPaintTab.tsx | 84 ++
src/components/PrintInstructions.tsx | 135 ++-
src/components/ThreeDControls.tsx | 121 +-
src/components/ThreeDView.tsx | 617 +++++-----
src/components/docs/DocsPage.tsx | 1 +
src/components/docs/MarkdownRenderer.tsx | 2 +-
src/docs/assets.ts | 8 +
src/hooks/useMultiHeadWorker.ts | 119 ++
src/hooks/useSwapPlan.ts | 116 +-
src/lib/autoPaint.ts | 8 +-
src/lib/export3mf.ts | 366 +++++-
src/lib/meshing.ts | 46 +-
src/lib/multiHeadAnalysis.ts | 466 ++++++++
src/lib/multiHeadAnalysisColorFirst.ts | 1007 +++++++++++++++++
src/lib/multiHeadSchedule.ts | 169 +++
src/lib/optimizer.ts | 4 +-
src/lib/patchedLayersToPlan.ts | 155 +++
src/lib/voxelMesh.ts | 132 +++
src/types/index.ts | 55 +-
src/workers/multiHead.worker.ts | 51 +
tests/export3mf.test.ts | 159 +++
tests/libModuleResolution.test.ts | 32 +
tests/meshingOptions.test.ts | 199 ++++
tests/multiHeadAnalysis.test.ts | 391 +++++++
tests/multiHeadAnalysisColorFirst.test.ts | 658 +++++++++++
tests/multiHeadSchedule.test.ts | 267 +++++
tests/patchedLayersToPlan.test.ts | 350 ++++++
32 files changed, 5829 insertions(+), 382 deletions(-)
create mode 100644 src/assets/diagrams/01_window_run_diagram.svg
create mode 100644 src/assets/diagrams/02_nozzle_swap_schedule.svg
create mode 100644 src/assets/diagrams/03_beer_lambert_blending.svg
create mode 100644 src/assets/diagrams/05_combo_search_space.svg
create mode 100644 src/hooks/useMultiHeadWorker.ts
create mode 100644 src/lib/multiHeadAnalysis.ts
create mode 100644 src/lib/multiHeadAnalysisColorFirst.ts
create mode 100644 src/lib/multiHeadSchedule.ts
create mode 100644 src/lib/patchedLayersToPlan.ts
create mode 100644 src/lib/voxelMesh.ts
create mode 100644 src/workers/multiHead.worker.ts
create mode 100644 tests/libModuleResolution.test.ts
create mode 100644 tests/meshingOptions.test.ts
create mode 100644 tests/multiHeadAnalysis.test.ts
create mode 100644 tests/multiHeadAnalysisColorFirst.test.ts
create mode 100644 tests/multiHeadSchedule.test.ts
create mode 100644 tests/patchedLayersToPlan.test.ts
diff --git a/src/App.tsx b/src/App.tsx
index 59dcd9f8..06dcc9b6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -31,6 +31,7 @@ import PreviewActions from './components/PreviewActions';
import { useDropzone } from './hooks/useDropzone';
import { exportObjectToStlBlob } from './lib/exportStl';
import { exportObjectTo3MFBlob } from './lib/export3mf';
+import { buildMultiHeadSchedule } from './lib/multiHeadSchedule';
import { useAppHandlers, type ExportProgressStep } from './hooks/useAppHandlers';
import { useProcessingState } from './hooks/useProcessingState';
import { useBuildWarning } from './hooks/useBuildWarning';
@@ -107,6 +108,9 @@ type AutoPaintPersisted = Pick<
| 'heightDithering'
| 'ditherLineWidth'
| 'flatPaint'
+ | 'multiHeadMode'
+ | 'multiHeadCount'
+ | 'multiHeadSearchDepth'
>;
// Schema v2: filament `td` values store frontlit hiding distances. State
@@ -170,6 +174,9 @@ const loadAutoPaintPersisted = (): AutoPaintPersisted | null => {
heightDithering: parsed.heightDithering ?? false,
ditherLineWidth: parsed.ditherLineWidth,
flatPaint: parsed.flatPaint ?? false,
+ multiHeadMode: parsed.multiHeadMode ?? false,
+ multiHeadCount: parsed.multiHeadCount ?? 4,
+ multiHeadSearchDepth: parsed.multiHeadSearchDepth ?? 'balanced',
};
} catch {
return null;
@@ -344,6 +351,9 @@ function App(): React.ReactElement | null {
heightDithering: autopaintHydrated.heightDithering ?? prev.heightDithering,
ditherLineWidth: autopaintHydrated.ditherLineWidth ?? prev.ditherLineWidth,
flatPaint: autopaintHydrated.flatPaint ?? prev.flatPaint,
+ multiHeadMode: autopaintHydrated.multiHeadMode ?? prev.multiHeadMode,
+ multiHeadCount: autopaintHydrated.multiHeadCount ?? prev.multiHeadCount,
+ multiHeadSearchDepth: autopaintHydrated.multiHeadSearchDepth ?? prev.multiHeadSearchDepth,
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -365,6 +375,9 @@ function App(): React.ReactElement | null {
heightDithering: threeDState.heightDithering,
ditherLineWidth: threeDState.ditherLineWidth,
flatPaint: threeDState.flatPaint,
+ multiHeadMode: threeDState.multiHeadMode,
+ multiHeadCount: threeDState.multiHeadCount,
+ multiHeadSearchDepth: threeDState.multiHeadSearchDepth,
});
}, [
threeDState.filaments,
@@ -379,6 +392,9 @@ function App(): React.ReactElement | null {
threeDState.heightDithering,
threeDState.ditherLineWidth,
threeDState.flatPaint,
+ threeDState.multiHeadMode,
+ threeDState.multiHeadCount,
+ threeDState.multiHeadSearchDepth,
]);
// No auto-build on tab switch — the user must click "Build 3D Model" / "Apply Changes".
@@ -541,8 +557,28 @@ function App(): React.ReactElement | null {
exportObjectTo3MFBlob(obj, {
layerHeight: builtModelState.layerHeight,
firstLayerHeight: builtModelState.slicerFirstLayerHeight,
- layerFilamentColors: builtModelAutoPaint
- ? builtModelState.autoPaintFilamentSwatches?.map((s) => s.hex)
+ layerFilamentColors:
+ builtModelAutoPaint
+ ? (builtModelState.patchedSliceData?.swatches
+ ?? builtModelState.autoPaintFilamentSwatches)?.map((s) => s.hex)
+ : undefined,
+ extruderCount: builtModelState.multiHeadMode
+ ? builtModelState.multiHeadCount
+ : undefined,
+ // Head Schedule swap checkpoints -> pause markers at those layers.
+ swapLayers: builtModelState.multiHeadMode
+ ? buildMultiHeadSchedule({
+ multiHeadWindows: builtModelState.multiHeadWindows,
+ nozzleAssignments: builtModelState.nozzleAssignments,
+ windowRunFilaments: builtModelState.windowRunFilaments,
+ nonWindowedRanges: builtModelState.nonWindowedRanges,
+ filaments: builtModelState.filaments,
+ })
+ ?.filter((e) => e.startLayer > 0 && e.swapCount > 0)
+ ?.map((e) => ({
+ layer: e.startLayer,
+ color: e.nozzles.find((n) => n.changed)?.filamentHex,
+ }))
: undefined,
onProgress,
onZipProgress,
@@ -793,20 +829,39 @@ function App(): React.ReactElement | null {
slicerFirstLayerHeight={
builtModelState.slicerFirstLayerHeight
}
- colorSliceHeights={builtModelState.colorSliceHeights}
- colorOrder={builtModelState.colorOrder}
- swatches={builtModelState.filteredSwatches}
+ colorSliceHeights={
+ builtModelState.patchedSliceData?.colorSliceHeights ??
+ builtModelState.colorSliceHeights
+ }
+ colorOrder={
+ builtModelState.patchedSliceData?.colorOrder ??
+ builtModelState.colorOrder
+ }
+ swatches={
+ builtModelAutoPaint && builtModelState.patchedSliceData
+ ? builtModelState.patchedSliceData.swatches
+ : builtModelState.filteredSwatches
+ }
filamentSwatches={
- builtModelAutoPaint
+ builtModelAutoPaint && !builtModelState.patchedSliceData
? builtModelState.autoPaintFilamentSwatches
: undefined
}
pixelSize={builtModelState.pixelSize}
rebuildSignal={threeDBuildSignal}
- autoPaintEnabled={builtModelAutoPaint}
- autoPaintTotalHeight={
- builtModelState.autoPaintResult?.totalHeight
+ perColorLayerColors={
+ builtModelAutoPaint
+ ? builtModelState.perColorLayerColors
+ : undefined
}
+ multiHeadWindows={builtModelState.multiHeadWindows}
+ colorLayerFilaments={builtModelState.colorLayerFilaments}
+ windowRunFilaments={builtModelState.windowRunFilaments}
+ nozzleAssignments={builtModelState.nozzleAssignments}
+ nonWindowedRanges={builtModelState.nonWindowedRanges}
+ filamentIds={builtModelState.filaments?.map((f) => f.id)}
+ autoPaintEnabled={builtModelAutoPaint}
+ autoPaintTotalHeight={builtModelState.autoPaintResult?.totalHeight}
autoPaintFilamentOrder={
builtModelState.autoPaintResult?.filamentOrder
}
diff --git a/src/assets/diagrams/01_window_run_diagram.svg b/src/assets/diagrams/01_window_run_diagram.svg
new file mode 100644
index 00000000..deec281c
--- /dev/null
+++ b/src/assets/diagrams/01_window_run_diagram.svg
@@ -0,0 +1,72 @@
+
\ No newline at end of file
diff --git a/src/assets/diagrams/02_nozzle_swap_schedule.svg b/src/assets/diagrams/02_nozzle_swap_schedule.svg
new file mode 100644
index 00000000..9656e643
--- /dev/null
+++ b/src/assets/diagrams/02_nozzle_swap_schedule.svg
@@ -0,0 +1,117 @@
+
\ No newline at end of file
diff --git a/src/assets/diagrams/03_beer_lambert_blending.svg b/src/assets/diagrams/03_beer_lambert_blending.svg
new file mode 100644
index 00000000..782ea50c
--- /dev/null
+++ b/src/assets/diagrams/03_beer_lambert_blending.svg
@@ -0,0 +1,51 @@
+
\ No newline at end of file
diff --git a/src/assets/diagrams/05_combo_search_space.svg b/src/assets/diagrams/05_combo_search_space.svg
new file mode 100644
index 00000000..787da3eb
--- /dev/null
+++ b/src/assets/diagrams/05_combo_search_space.svg
@@ -0,0 +1,180 @@
+
\ No newline at end of file
diff --git a/src/components/AutoPaintTab.tsx b/src/components/AutoPaintTab.tsx
index 7f808827..dfffbe8e 100644
--- a/src/components/AutoPaintTab.tsx
+++ b/src/components/AutoPaintTab.tsx
@@ -169,6 +169,14 @@ interface AutoPaintTabProps {
setOptimizerSeed: (v: number | undefined) => void;
regionWeightingMode: 'uniform' | 'center' | 'edge';
setRegionWeightingMode: (v: 'uniform' | 'center' | 'edge') => void;
+
+ // Multi-head mode
+ multiHeadMode: boolean;
+ setMultiHeadMode: (v: boolean) => void;
+ multiHeadCount: number;
+ setMultiHeadCount: (v: number) => void;
+ multiHeadSearchDepth: 'fast' | 'balanced' | 'thorough';
+ setMultiHeadSearchDepth: (v: 'fast' | 'balanced' | 'thorough') => void;
}
export default function AutoPaintTab({
@@ -228,6 +236,12 @@ export default function AutoPaintTab({
setOptimizerSeed,
regionWeightingMode,
setRegionWeightingMode,
+ multiHeadMode,
+ setMultiHeadMode,
+ multiHeadCount,
+ setMultiHeadCount,
+ multiHeadSearchDepth,
+ setMultiHeadSearchDepth,
}: AutoPaintTabProps) {
const {
result: nextBestResult,
@@ -963,6 +977,76 @@ export default function AutoPaintTab({
)}
+ {/* Multi-head mode */}
+ {filaments.length > 0 && (
+
+
+
+
+
+
+ Optimize layer order per-pixel across N heads ({filaments.length > 0 ? `${Math.min(filaments.length, 5)}^${Math.min(filaments.length, 5)} = ${Math.pow(Math.min(filaments.length, 5), Math.min(filaments.length, 5)).toLocaleString()} color combinations` : 'load filaments to see'})
+
+
+
+
+ {multiHeadMode && (
+
+
+
+ {
+ const v = parseInt(e.target.value, 10);
+ if (v >= 2) setMultiHeadCount(v);
+ }}
+ />
+
+
+
+
+
+
+ )}
+
+ )}
+
{/* Auto-paint transition zones preview */}
{autoPaintResult && autoPaintResult.transitionZones.length > 0 && (
<>
diff --git a/src/components/PrintInstructions.tsx b/src/components/PrintInstructions.tsx
index f2b55c98..c94203c5 100644
--- a/src/components/PrintInstructions.tsx
+++ b/src/components/PrintInstructions.tsx
@@ -1,8 +1,10 @@
import { CollapsibleCard, DirtyDot } from '@/components/CollapsibleCard';
-import type { SwapEntry } from '../hooks/useSwapPlan';
+import type { SwapEntry, MultiHeadScheduleEvent } from '../hooks/useSwapPlan';
interface PrintInstructionsProps {
swapPlan: SwapEntry[];
+ multiHeadPlan?: MultiHeadScheduleEvent[] | null;
+ multiHeadMode?: boolean;
layerHeight: number;
slicerFirstLayerHeight: number;
copied: boolean;
@@ -15,6 +17,8 @@ interface PrintInstructionsProps {
export default function PrintInstructions({
swapPlan,
+ multiHeadPlan,
+ multiHeadMode = false,
layerHeight,
slicerFirstLayerHeight,
copied,
@@ -39,7 +43,6 @@ export default function PrintInstructions({
onClick={onCopy}
title="Copy print instructions to clipboard"
aria-pressed={copied}
- disabled={tooManyColors}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200 ${
copied
? 'bg-green-600 text-white'
@@ -51,27 +54,24 @@ export default function PrintInstructions({
}
>
+ {tooManyColors && (
+
+ This print has {colorCount} layers — swap instructions may be slow to generate above 64.
+
+ )}
{/* Recommended Settings */}
Recommended Settings
-
- • Wall loops: 1
-
-
- • Infill: 100%
-
+
• Wall loops: 1
+
• Infill: 100%
• Layer height:{' '}
-
- {layerHeight.toFixed(3)} mm
-
+ {layerHeight.toFixed(3)} mm
• First layer height:{' '}
-
- {slicerFirstLayerHeight.toFixed(3)} mm
-
+ {slicerFirstLayerHeight.toFixed(3)} mm
@@ -100,18 +100,18 @@ export default function PrintInstructions({
After printing, flip the piece over to view the image.
+ ) : multiHeadPlan ? (
+
+ ) : multiHeadMode ? (
+
+ Click Build 3D Model to generate the multi-head schedule.
+
) : (
+ /* Single-head */
<>
- {/* Start Color */}
-
- Start with Color
-
- {tooManyColors ? (
-
- —
-
- ) : swapPlan.length && swapPlan[0].type === 'start' ? (
+
Start with Color
+ {swapPlan.length && swapPlan[0].type === 'start' ? (
(() => {
const sw = swapPlan[0].swatch;
return (
@@ -128,24 +128,12 @@ export default function PrintInstructions({
);
})()
) : (
-
- —
-
+
—
)}
-
- {/* Color Swap Plan */}
-
- Color Swap Plan
-
- {tooManyColors ? (
-
- Swap instructions are disabled for very large palettes (
- {colorCount} colors). Reduce the image to 64 colors or fewer in
- 2D mode first.
-
- ) : swapPlan.length <= 1 ? (
+
Color Swap Plan
+ {swapPlan.length <= 1 ? (
Only one color configured — no swaps needed.
@@ -203,3 +191,74 @@ export default function PrintInstructions({
);
}
+
+// ---------------------------------------------------------------------------
+// HeadSchedule — multi-head load / swap schedule in layer order
+// ---------------------------------------------------------------------------
+
+function HeadSchedule({ events }: { events: MultiHeadScheduleEvent[] }) {
+ if (events.length === 0) {
+ return (
+
+ No head assignments computed yet.
+
+ );
+ }
+
+ return (
+
+
Head Schedule
+
+ {events.filter(evt => evt.isPrePrint || evt.swapCount > 0).map((evt, evtIdx) => {
+ return (
+
+ {/* Event header */}
+
+ {evt.isPrePrint
+ ? <>Before print — load all heads>
+ : <>Layer {evt.startLayer} — swap {evt.swapCount} head{evt.swapCount !== 1 ? 's' : ''}>
+ }
+
+
+ {/* Nozzle rows — all heads shown; changed ones highlighted */}
+
+ {evt.nozzles.map((n) => (
+
+
+ Head {n.nozzle}
+
+
+
+ {n.filamentName}
+
+
+ ))}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/ThreeDControls.tsx b/src/components/ThreeDControls.tsx
index 0cd03793..81d6d8df 100644
--- a/src/components/ThreeDControls.tsx
+++ b/src/components/ThreeDControls.tsx
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
import { Check, RotateCcw, Loader2 } from 'lucide-react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { autoPaintToSliceHeights } from '../lib/autoPaint';
+import { patchedLayersToPlan, patchedLayersToSliceData, buildPerColorLayerColors } from '../lib/patchedLayersToPlan';
import {
loadPrintSettingsFromStorage,
savePrintSettingsToStorage,
@@ -16,6 +17,7 @@ import { useProfileManager } from '../hooks/useProfileManager';
import { useColorSlicing } from '../hooks/useColorSlicing';
import { useSwapPlan } from '../hooks/useSwapPlan';
import { useAutoPaintWorker } from '../hooks/useAutoPaintWorker';
+import { useMultiHeadWorker } from '../hooks/useMultiHeadWorker';
import type {
AutoPaintRepeatLimit,
AutoPaintTransitionOpacity,
@@ -150,6 +152,13 @@ export default function ThreeDControls({
persisted?.regionWeightingMode ?? 'uniform'
);
+ // --- Multi-head mode ---
+ const [multiHeadMode, setMultiHeadMode] = useState(persisted?.multiHeadMode ?? false);
+ const [multiHeadCount, setMultiHeadCount] = useState(persisted?.multiHeadCount ?? 4);
+ const [multiHeadSearchDepth, setMultiHeadSearchDepth] = useState<'fast' | 'balanced' | 'thorough'>(
+ persisted?.multiHeadSearchDepth ?? 'balanced'
+ );
+
const handleEnhancedColorMatchChange = useCallback((v: boolean) => {
setEnhancedColorMatch(v);
if (!v) {
@@ -202,6 +211,9 @@ export default function ThreeDControls({
optimizerSeed,
regionWeightingMode,
smoothMeshing,
+ multiHeadMode,
+ multiHeadCount,
+ multiHeadSearchDepth,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
@@ -218,6 +230,9 @@ export default function ThreeDControls({
optimizerSeed,
regionWeightingMode,
smoothMeshing,
+ multiHeadMode,
+ multiHeadCount,
+ multiHeadSearchDepth,
]);
useEffect(() => {
@@ -280,6 +295,12 @@ export default function ThreeDControls({
});
const autoPaintProgressPercent = Math.round(Math.max(0, Math.min(1, autoPaintProgress)) * 100);
+ // --- Multi-head analysis (runs in Web Worker to avoid blocking the UI) ---
+ const {
+ isComputing: isMultiHeadComputing,
+ run: runMultiHead,
+ } = useMultiHeadWorker();
+
const autoPaintSliceData = useMemo(() => {
if (!autoPaintResult) return undefined;
return autoPaintToSliceHeights(autoPaintResult, layerHeight, slicerFirstLayerHeight);
@@ -344,7 +365,7 @@ export default function ThreeDControls({
const isInstructionOverLimit = instructionColorCount > 64;
// --- Swap Plan ---
- const { swapPlan, copied, copyToClipboard } = useSwapPlan({
+ const { swapPlan, multiHeadPlan, copied, copyToClipboard } = useSwapPlan({
colorOrder: instructionColorOrder,
colorSliceHeights: instructionColorSliceHeights,
filtered: instructionFiltered,
@@ -352,14 +373,60 @@ export default function ThreeDControls({
slicerFirstLayerHeight: instructionSlicerFirstLayerHeight,
paintMode: instructionPaintMode,
autoPaintResult: instructionAutoPaintResult,
- disabled: isInstructionOverLimit,
+ multiHeadWindows: persisted?.multiHeadWindows,
+ patchedTransitionZones: persisted?.patchedTransitionZones,
+ nozzleAssignments: persisted?.nozzleAssignments,
+ windowRunFilaments: persisted?.windowRunFilaments,
+ preWindowFilaments: persisted?.preWindowFilaments,
+ nonWindowedRanges: persisted?.nonWindowedRanges,
+ filaments,
flatPaint: instructionFlatPaint,
});
// --- Apply handler ---
- const handleApply = useCallback(() => {
+ const handleApply = useCallback(async () => {
if (!onChange) return;
+ // Run the appropriate multi-head optimizer based on the selected mode.
+ let activeResult = null;
+ if (multiHeadMode && paintMode === 'autopaint' && autoPaintResult) {
+ // `filtered` carries SwatchEntry objects at runtime (with pixel-frequency
+ // `count`), even though the prop is typed as the narrower Swatch.
+ const swatches = filtered.map((s) => ({
+ hex: s.hex,
+ count: (s as { count?: number }).count,
+ }));
+ activeResult = await runMultiHead({
+ filaments,
+ autoPaintResult,
+ imageSwatches: swatches,
+ layerHeight,
+ firstLayerHeight: slicerFirstLayerHeight,
+ n: multiHeadCount,
+ searchDepth: multiHeadSearchDepth,
+ });
+ }
+
+ const newMultiHeadWindows = activeResult?.windows ?? [];
+ const patchedTransitionZones = activeResult && activeResult.patchedLayers.length > 0
+ ? patchedLayersToPlan(activeResult.patchedLayers, filaments)
+ : undefined;
+ const patchedSliceData = activeResult && activeResult.patchedLayers.length > 0
+ ? patchedLayersToSliceData(activeResult.patchedLayers, filaments, slicerFirstLayerHeight)
+ : undefined;
+ const perColorLayerColors = activeResult && activeResult.patchedLayers.length > 0
+ ? buildPerColorLayerColors(activeResult.patchedLayers, activeResult.colorLayerFilaments, filaments)
+ : undefined;
+ // Per-colour filament-index-per-layer map. ThreeDView needs this (together with
+ // the window/nozzle data below) to resolve each sub-mesh's physical nozzle; if it
+ // isn't persisted, nozzle tagging silently no-ops and export3mf falls back to
+ // colour-order extruders and all-white filament colours.
+ const colorLayerFilaments = activeResult?.colorLayerFilaments;
+ const windowRunFilaments = activeResult?.windowRunFilaments;
+ const nozzleAssignments = activeResult?.nozzleAssignments;
+ const preWindowFilaments = activeResult?.preWindowFilaments;
+ const nonWindowedRanges = activeResult?.nonWindowedRanges;
+
if (paintMode === 'autopaint' && autoPaintSliceData && autoPaintResult) {
onChange({
layerHeight,
@@ -385,6 +452,18 @@ export default function ThreeDControls({
autoPaintFilamentSwatches: autoPaintSliceData.filamentSwatches,
calibrationLayerHeight,
smoothMeshing,
+ multiHeadMode,
+ multiHeadCount,
+ multiHeadSearchDepth,
+ multiHeadWindows: newMultiHeadWindows,
+ patchedTransitionZones,
+ patchedSliceData,
+ perColorLayerColors,
+ colorLayerFilaments,
+ windowRunFilaments,
+ nozzleAssignments,
+ preWindowFilaments,
+ nonWindowedRanges,
});
} else {
onChange({
@@ -402,6 +481,18 @@ export default function ThreeDControls({
regionWeightingMode,
calibrationLayerHeight,
smoothMeshing,
+ multiHeadMode,
+ multiHeadCount,
+ multiHeadSearchDepth,
+ multiHeadWindows: newMultiHeadWindows,
+ patchedTransitionZones,
+ patchedSliceData,
+ perColorLayerColors,
+ colorLayerFilaments,
+ windowRunFilaments,
+ nozzleAssignments,
+ preWindowFilaments,
+ nonWindowedRanges,
});
}
}, [
@@ -428,6 +519,10 @@ export default function ThreeDControls({
smoothMeshing,
autoPaintResult,
autoPaintSliceData,
+ multiHeadMode,
+ multiHeadCount,
+ multiHeadSearchDepth,
+ runMultiHead,
]);
return (
@@ -437,10 +532,10 @@ export default function ThreeDControls({