Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/components/PerspectiveControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { MockupSettings, PerspectiveCorners } from "../types";
import { isValidPerspectiveCorners } from "../lib/perspective";

interface PerspectiveControlsProps {
settings: MockupSettings;
onChange: (settings: MockupSettings) => void;
mode: "psd" | "image";
}

function cornersFromPlacement(settings: MockupSettings): PerspectiveCorners {
const left = Math.max(0, Math.min(100, settings.areaLeftPercent));
const top = Math.max(0, Math.min(100, settings.areaTopPercent));
const right = Math.max(left + 1, Math.min(100, settings.areaLeftPercent + settings.areaWidthPercent));
const bottom = Math.max(top + 1, Math.min(100, settings.areaTopPercent + settings.areaHeightPercent));
return {
topLeft: { x: left, y: top },
topRight: { x: right, y: top },
bottomRight: { x: right, y: bottom },
bottomLeft: { x: left, y: bottom },
};
}

export function PerspectiveControls({ settings, onChange, mode }: PerspectiveControlsProps) {
if (mode !== "image") return null;

const enabled = Boolean(settings.perspective?.enabled);

function enablePerspective(): void {
const existing = settings.perspective?.corners;
const corners = existing && isValidPerspectiveCorners(existing)
? existing
: cornersFromPlacement(settings);
onChange({ ...settings, perspective: { enabled: true, corners } });
}

function disablePerspective(): void {
if (!settings.perspective) return;
onChange({ ...settings, perspective: { ...settings.perspective, enabled: false } });
}

function resetPerspective(): void {
onChange({ ...settings, perspective: { enabled: true, corners: cornersFromPlacement(settings) } });
}

return (
<section className="compact-panel">
<div className="section-title-row">
<h3>4-Corner Perspective</h3>
<span className="tiny-status">{enabled ? "Enabled" : "Optional"}</span>
</div>
<p className="hint-text">
Warp the design onto angled signs, frames, screens or packaging. When enabled, drag the four teal corners directly in the editor.
</p>
<div className="quick-actions-grid">
<button type="button" className={enabled ? "is-active" : ""} onClick={enabled ? disablePerspective : enablePerspective}>
{enabled ? "Disable Perspective" : "Enable Perspective"}
</button>
<button type="button" onClick={resetPerspective} disabled={!enabled}>
Reset Corners
</button>
</div>
{enabled ? (
<p className="muted small-text">
Perspective uses the four corners as the complete design surface. Move/scale/rotate controls are ignored until perspective is disabled.
</p>
) : null}
</section>
);
}
55 changes: 32 additions & 23 deletions src/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DocumentSize, MockupSettings, SavedPreset } from "../types";
import type { SmartObjectBounds, SmartObjectCandidate } from "../lib/psdSmartObjectDetection";
import { shirtPlacementDefaults } from "../lib/config/defaults";
import { PerspectiveControls } from "./PerspectiveControls";

interface SettingsPanelProps {
settings: MockupSettings;
Expand Down Expand Up @@ -149,35 +150,43 @@ export function SettingsPanel({
</div>
</section>

<PerspectiveControls settings={settings} onChange={onChange} mode={mode} />

<section className="compact-panel">
<div className="section-title-row">
<h3>Design Transform</h3>
<span className="tiny-status">Editable</span>
<span className="tiny-status">{settings.perspective?.enabled && isImageMode ? "Paused by perspective" : "Editable"}</span>
</div>

<div className="slider-group slider-group--stacked">
<SliderField label="Horizontal Offset" suffix="%" min={-150} max={150} value={settings.left} onChange={(left) => patch({ left })} />
<SliderField label="Vertical Offset" suffix="%" min={-150} max={150} value={settings.top} onChange={(top) => patch({ top })} />
<SliderField label="Scale" suffix="%" min={1} max={250} value={uniformScale} onChange={setUniformScale} />
<SliderField label="Rotation" suffix="°" min={-180} max={180} value={settings.rotation} onChange={(rotation) => patch({ rotation })} />
<SliderField label="Opacity" suffix="%" min={0} max={100} value={settings.opacity} onChange={(opacity) => patch({ opacity })} />
</div>
{settings.perspective?.enabled && isImageMode ? (
<p className="hint-text">Disable 4-Corner Perspective to use move, scale, rotation and fit controls again.</p>
) : (
<>
<div className="slider-group slider-group--stacked">
<SliderField label="Horizontal Offset" suffix="%" min={-150} max={150} value={settings.left} onChange={(left) => patch({ left })} />
<SliderField label="Vertical Offset" suffix="%" min={-150} max={150} value={settings.top} onChange={(top) => patch({ top })} />
<SliderField label="Scale" suffix="%" min={1} max={250} value={uniformScale} onChange={setUniformScale} />
<SliderField label="Rotation" suffix="°" min={-180} max={180} value={settings.rotation} onChange={(rotation) => patch({ rotation })} />
<SliderField label="Opacity" suffix="%" min={0} max={100} value={settings.opacity} onChange={(opacity) => patch({ opacity })} />
</div>

<div className="segmented-group">
<span>Fit Mode</span>
<div className="segmented segmented--wide">
{fitModes.map((mode) => (
<button
className={settings.fitMode === mode.id ? "is-active" : ""}
key={mode.id}
type="button"
onClick={() => patch({ fitMode: mode.id })}
>
{mode.label}
</button>
))}
</div>
</div>
<div className="segmented-group">
<span>Fit Mode</span>
<div className="segmented segmented--wide">
{fitModes.map((fitMode) => (
<button
className={settings.fitMode === fitMode.id ? "is-active" : ""}
key={fitMode.id}
type="button"
onClick={() => patch({ fitMode: fitMode.id })}
>
{fitMode.label}
</button>
))}
</div>
</div>
</>
)}
</section>

<details className="details-card">
Expand Down
Loading