Skip to content

Commit

Permalink
Fix multimaterial dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
ochafik committed Dec 24, 2024
1 parent dc5be49 commit e9d46a6
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 14 deletions.
3 changes: 3 additions & 0 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export function App({initialState, statePersister, fs}: {initialState: State, st
} else if (event.key === 'F6') {
event.preventDefault();
model.render({isPreview: false, now: true})
} else if (event.key === 'F7') {
event.preventDefault();
model.export();
}
};
window.addEventListener('keydown', handleKeyDown);
Expand Down
12 changes: 10 additions & 2 deletions src/components/ExportButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useContext } from 'react';
import React, { useContext, useState } from 'react';
import { ModelContext } from './contexts';

import { SplitButton } from 'primereact/splitbutton';
import { MenuItem } from 'primereact/menuitem';

type ExtendedMenuItem = MenuItem & { buttonLabel: string };
type ExtendedMenuItem = MenuItem & { buttonLabel?: string };

export default function ExportButton({className, style}: {className?: string, style?: React.CSSProperties}) {
const model = useContext(ModelContext);
Expand Down Expand Up @@ -56,6 +56,14 @@ export default function ExportButton({className, style}: {className?: string, st
icon: 'pi pi-file',
command: () => model!.setFormats(undefined, '3mf'),
},
{
separator: true
},
{
label: 'Edit materials' + ((state.params.extruderColors ?? []).length > 0 ? ` (${(state.params.extruderColors ?? []).length})` : ''),
icon: 'pi pi-cog',
command: () => model!.mutate(s => s.view.extruderPickerVisibility = 'editing'),
}
];

const exportFormat = state.is2D ? state.params.exportFormat2D : state.params.exportFormat3D;
Expand Down
4 changes: 2 additions & 2 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Toast } from 'primereact/toast';
import HelpMenu from './HelpMenu';
import ExportButton from './ExportButton';
import SettingsMenu from './SettingsMenu';
import MultimaterialColorsDialog from './MultimaterialColorsDialog';


export default function Footer({style}: {style?: CSSProperties}) {
Expand All @@ -19,7 +20,6 @@ export default function Footer({style}: {style?: CSSProperties}) {

const toast = useRef<Toast>(null);


const severityByMarkerSeverity = new Map<monaco.MarkerSeverity, 'danger' | 'warning' | 'info'>([
[monaco.MarkerSeverity.Error, 'danger'],
[monaco.MarkerSeverity.Warning, 'warning'],
Expand Down Expand Up @@ -72,7 +72,7 @@ export default function Footer({style}: {style?: CSSProperties}) {
/>
) : undefined
}
{/* <RenderExportButton /> */}
<MultimaterialColorsDialog />
{/* <Button
icon="pi pi-bolt"
onClick={() => model.render({isPreview: false, now: true})}
Expand Down
119 changes: 119 additions & 0 deletions src/components/MultimaterialColorsDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import chroma from 'chroma-js';
import React, { useContext, useState } from 'react';
import { ColorPicker } from 'primereact/colorpicker';
import { Button } from 'primereact/button';
import { InputText } from 'primereact/inputtext';
import { ModelContext } from './contexts';
import { Dialog } from 'primereact/dialog';

export default function MultimaterialColorsDialog() {
const model = useContext(ModelContext);
if (!model) throw new Error('No model');
const state = model.state;

const [tempExtruderColors, setTempExtruderColors] = useState<string[]>(state.params.extruderColors ?? []);

function setColor(index: number, color: string) {
setTempExtruderColors(tempExtruderColors.map((c, i) => i === index ? color : c));
}
function removeColor(index: number) {
setTempExtruderColors(tempExtruderColors.filter((c, i) => i !== index));
}
function addColor() {
setTempExtruderColors([...tempExtruderColors, '']);
}

const cancelExtruderPicker = () => {
setTempExtruderColors(state.params.extruderColors ?? []);
model!.mutate(s => s.view.extruderPickerVisibility = undefined);
};
const canAddColor = !tempExtruderColors.some(c => c.trim() === '');

return (
<Dialog
header="Multimaterial Color Picker"
visible={!!state.view.extruderPickerVisibility}
onHide={cancelExtruderPicker}
footer={
<div>
<Button label="Cancel" icon="pi pi-times" onClick={cancelExtruderPicker} className="p-button-text" />
<Button
label={state.view.extruderPickerVisibility == 'exporting' ? "Export" : "Save"}
icon="pi pi-check"
disabled={!tempExtruderColors.every(c => chroma.valid(c))}
autoFocus
onClick={e => {
model!.mutate(s => {
s.params.extruderColors = tempExtruderColors;
s.view.extruderPickerVisibility = undefined;
});
if (state.view.extruderPickerVisibility === 'exporting') {
model!.export();
}
}} />
</div>
}
>
<div className="flex flex-column align-items-center">
<div>
To print on a multimaterial printer using PrusaSlicer, BambuSlicer or OrcaSlicer, we map the model's colors to the closest match in the list of extruder colors.
</div>
<div>
Please define the colors of your extruders below.
</div>

<div className="p-4">

<div className="flex flex-wrap gap-2" style={{
flexDirection: 'column',
}}>
{tempExtruderColors.map((color, index) => (
<div key={index} className="flex items-center gap-2">
<ColorPicker
value={chroma.valid(color) ? chroma(color).hex() : 'black'}
onChange={(e) => e.value && setColor(index, chroma(e.value.toString()).name())}
/>
<InputText
value={color}
autoFocus={color === ''}
invalid={color.trim() === '' || !chroma.valid(color)}
onKeyDown={(e) => {
if (e.key === 'Enter' && canAddColor) {
e.preventDefault();
addColor();
}
}}
onChange={(e) => {
let color = e.target.value;
try {
color = chroma(e.target.value).name();
console.log(`color: ${e.target.value} -> ${color}`);
} catch (e) {
// ignore
console.error(e);
}
setColor(index, color);
}}
/>
<Button
icon="pi pi-times"
text
onClick={() => removeColor(index)}
className="p-button-danger p-button-sm"
/>
</div>
))}
<div>
<Button
label="Add Color"
disabled={!canAddColor}
icon="pi pi-plus"
text
onClick={addColor} className="p-button-sm" />
</div>
</div>
</div>
</div>
</Dialog>
);
}
23 changes: 20 additions & 3 deletions src/io/export_3mf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,28 @@ function getColorMapping(colors: chroma.Color[], projectedColors: chroma.Color[]
// Reverse-engineered from PrusaSlicer / BambuStudio's output.
const PAINT_COLOR_MAP = ['', '8', '0C', '1C', '2C', '3C', '4C', '5C', '6C', '7C', '8C', '9C', 'AC', 'BC', 'CC', 'DC'];

export function exportOffTo3MF(data: IndexedPolyhedron, extruderColors?: chroma.Color[]): Blob {
export function export3MF(data: IndexedPolyhedron, extruderColors?: chroma.Color[]): Blob {
const objectUuid = uuidv4();
const buildUuid = uuidv4();
const paintColorByColorIndex = extruderColors &&
getColorMapping(data.colors.map(c => chroma.rgb(...c)), extruderColors).map(i => PAINT_COLOR_MAP[i]);

const dataColors = data.colors.map(([r, g, b, a]) => chroma.rgb(r*255, g*255, b*255, a*255));
const extruderIndexByColorIndex = extruderColors &&
getColorMapping(dataColors, extruderColors);

if (extruderColors) {
console.log('Extruder colors:');
for (const c of extruderColors) {
console.log(`- ${c.name()}`);
}
console.log('Model color mapping:');
dataColors.forEach((from, i) => {
const extruderIndex = extruderIndexByColorIndex![i];
const to = extruderColors[extruderIndex];
console.log(`- ${from.name()} -> ${to?.name()} (${PAINT_COLOR_MAP[extruderIndex]})`);
});
}

const paintColorByColorIndex = extruderIndexByColorIndex?.map(i => PAINT_COLOR_MAP[i]);

const archive = {
'3D/3dmodel.model': new TextEncoder().encode([
Expand Down
3 changes: 2 additions & 1 deletion src/io/import_off.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export function parseOff(content: string): IndexedPolyhedron {
let colorIndex = colorMap.get(colorKey);
if (colorIndex == null) {
colorIndex = colors.length;
colors.push(color);
const [r, g, b, a] = color;
colors.push([r, g, b, a ?? 1]);
colorMap.set(colorKey, colorIndex);
}

Expand Down
3 changes: 2 additions & 1 deletion src/state/app-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ export interface State {
features: string[],
exportFormat2D: keyof typeof VALID_EXPORT_FORMATS_2D,
exportFormat3D: keyof typeof VALID_EXPORT_FORMATS_3D,
extruderColors?: string[],
},

view: {
logs?: boolean,
extruderPicker?: boolean,
extruderPickerVisibility?: 'editing' | 'exporting',
layout: {
mode: 'single',
focus: SingleLayoutComponentId,
Expand Down
7 changes: 4 additions & 3 deletions src/state/fragment-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,13 @@ export async function readStateFromFragment(): Promise<State | null> {
vars: params?.vars, // TODO: validate!
// Source deserialization also handles legacy links (source + sourcePath)
sources: params?.sources ?? (params?.source ? [{path: params?.sourcePath, content: params?.source}] : undefined), // TODO: validate!
exportFormat2D: validateStringEnum(params?.exportFormat, Object.keys(VALID_EXPORT_FORMATS_2D), s => 'svg'),
exportFormat3D: validateStringEnum(params?.exportFormat, Object.keys(VALID_EXPORT_FORMATS_3D), s => 'glb'),
exportFormat2D: validateStringEnum(params?.exportFormat2D, Object.keys(VALID_EXPORT_FORMATS_2D), s => 'svg'),
exportFormat3D: validateStringEnum(params?.exportFormat3D, Object.keys(VALID_EXPORT_FORMATS_3D), s => 'glb'),
extruderColors: validateArray(params?.extruderColors, validateString, () => undefined as any as []),
},
view: {
logs: validateBoolean(view?.logs),
extruderPicker: validateBoolean(view?.extruderPicker),
extruderPickerVisibility: validateStringEnum(view?.extruderPickerVisibility, ['editing', 'exporting'], s => undefined),
layout: {
mode: validateStringEnum(view?.layout?.mode, ['multi', 'single']),
focus: validateStringEnum(view?.layout?.focus, ['editor', 'viewer', 'customizer'], s => false),
Expand Down
9 changes: 7 additions & 2 deletions src/state/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { ProcessStreams } from "../runner/openscad-runner";
import { is2DFormatExtension } from "./formats";
import { parseOff } from "../io/import_off";
import { exportGlb } from "../io/export_glb";
import { exportOffTo3MF as export3MF } from "../io/export_3mf";
import { export3MF } from "../io/export_3mf";
import chroma from "chroma-js";

export class Model {
constructor(private fs: FS, public state: State, private setStateCallback?: (state: State) => void,
Expand Down Expand Up @@ -208,6 +209,10 @@ export class Model {
return;
}
}
if (!this.state.is2D && this.state.params.exportFormat3D == '3mf' && !this.state.params.extruderColors) {
this.mutate(s => this.state.view.extruderPickerVisibility = 'exporting');
return;
}
this.mutate(s => {
s.currentRunLogs ??= [];
s.exporting = true;
Expand All @@ -225,7 +230,7 @@ export class Model {
if (exportFormat === '3mf') {
const start = performance.now();
const data = parseOff(await this.state.output.outFile.text());
const exportedData = export3MF(data);
const exportedData = export3MF(data, this.state.params.extruderColors?.map(c => chroma(c)));
const elapsedMillis = performance.now() - start;
output = {
outFile: new File([exportedData], this.state.output.outFile.name.replace('.off', '.3mf')),
Expand Down

0 comments on commit e9d46a6

Please sign in to comment.