89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
import type { TextureAsset } from '../../core/model/Asset';
|
||
import { DEFAULT_SLICE, type SliceOptions, sliceFrames } from '../../core/model/texture';
|
||
import { type Button, Checkbox, Label, NumberField, Spacer, Theme, type Widget } from '../../ui';
|
||
import { FormDialog } from '../FormDialog';
|
||
|
||
const FIELD_WIDTH = 76;
|
||
|
||
/**
|
||
* Cuts a sheet into a grid, asked for as columns and rows rather than a cell size: the counts are
|
||
* what the user knows, and a cell size derived from the picture cannot disagree with them.
|
||
*/
|
||
export class SliceDialog extends FormDialog {
|
||
private readonly options: SliceOptions = { ...DEFAULT_SLICE };
|
||
private replace = true;
|
||
private readonly summary: Label;
|
||
private readonly confirm: Button;
|
||
|
||
constructor(
|
||
private readonly asset: TextureAsset,
|
||
onSlice: (options: SliceOptions, replace: boolean) => void,
|
||
) {
|
||
super(340, `Slice "${asset.name}" into a grid`);
|
||
this.content.add(new Label(`Picture is ${asset.width} × ${asset.height} px`, { style: 'muted' }));
|
||
|
||
this.content.add(this.pair('Columns', 'columns', 1, 'Rows', 'rows', 1));
|
||
this.content.add(this.pair('Offset X', 'offsetX', 0, 'Offset Y', 'offsetY', 0));
|
||
this.content.add(this.pair('Spacing X', 'spacingX', 0, 'Spacing Y', 'spacingY', 0));
|
||
|
||
const box = new Checkbox(this.replace);
|
||
box.onChange = (value) => {
|
||
this.replace = Boolean(value);
|
||
};
|
||
this.content.add(
|
||
this.formRow(box, new Label('Replace the frames this texture already has', { style: 'muted' })),
|
||
);
|
||
|
||
this.summary = new Label('', { style: 'muted' });
|
||
this.content.add(this.summary);
|
||
|
||
this.confirm = this.addButtons('Slice', 'primary', () => onSlice({ ...this.options }, this.replace));
|
||
|
||
this.refreshSummary();
|
||
}
|
||
|
||
private pair(
|
||
labelA: string,
|
||
keyA: keyof SliceOptions,
|
||
minA: number,
|
||
labelB: string,
|
||
keyB: keyof SliceOptions,
|
||
minB: number,
|
||
): Widget {
|
||
return this.formRow(
|
||
this.fieldLabel(labelA),
|
||
this.fieldFor(keyA, minA),
|
||
new Spacer(Theme.padding),
|
||
this.fieldLabel(labelB),
|
||
this.fieldFor(keyB, minB),
|
||
);
|
||
}
|
||
|
||
private fieldFor(key: keyof SliceOptions, min: number): NumberField {
|
||
const field = new NumberField(this.options[key], {
|
||
min,
|
||
step: 1,
|
||
decimals: 0,
|
||
onChange: (value) => {
|
||
this.options[key] = Math.round(Number(value));
|
||
this.refreshSummary();
|
||
},
|
||
});
|
||
field.minWidth = FIELD_WIDTH;
|
||
return field;
|
||
}
|
||
|
||
private refreshSummary(): void {
|
||
const rects = sliceFrames(this.asset.width, this.asset.height, this.options);
|
||
const first = rects[0];
|
||
this.summary.setText(
|
||
first
|
||
? `${rects.length} frames of ${first.w} × ${first.h} px`
|
||
: 'These numbers do not fit the picture – no frames.',
|
||
);
|
||
this.summary.setStyle(first ? 'muted' : 'body');
|
||
this.confirm.setEnabled(rects.length > 0);
|
||
this.refreshLayout();
|
||
}
|
||
}
|