Make UV Editor canvas dimensions strictly follow the base texture aspect ratio
This commit is contained in:
+20
-18
@@ -40,6 +40,8 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
|||||||
const [mappingType, setMappingType] = useState<string>(targetObject?.materialProps?.mappingType || 'uv');
|
const [mappingType, setMappingType] = useState<string>(targetObject?.materialProps?.mappingType || 'uv');
|
||||||
const [bgColor, setBgColor] = useState(initBg);
|
const [bgColor, setBgColor] = useState(initBg);
|
||||||
const [bgTransparent, setBgTransparent] = useState(initTrans);
|
const [bgTransparent, setBgTransparent] = useState(initTrans);
|
||||||
|
const [canvasW, setCanvasW] = useState(1024);
|
||||||
|
const [canvasH, setCanvasH] = useState(1024);
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -90,9 +92,9 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
|||||||
|
|
||||||
const renderCanvas = useCallback((lrs: Layer[], transparent: boolean, bg: string): string => {
|
const renderCanvas = useCallback((lrs: Layer[], transparent: boolean, bg: string): string => {
|
||||||
const c = document.createElement('canvas');
|
const c = document.createElement('canvas');
|
||||||
c.width = c.height = CANVAS_SIZE;
|
c.width = canvasW; c.height = canvasH;
|
||||||
const ctx = c.getContext('2d')!;
|
const ctx = c.getContext('2d')!;
|
||||||
if (!transparent) { ctx.fillStyle = bg; ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); }
|
if (!transparent) { ctx.fillStyle = bg; ctx.fillRect(0, 0, canvasW, canvasH); }
|
||||||
lrs.forEach(layer => {
|
lrs.forEach(layer => {
|
||||||
if (!layer.img || layer.visible === false) return;
|
if (!layer.img || layer.visible === false) return;
|
||||||
const iw = layer.img.width, ih = layer.img.height;
|
const iw = layer.img.width, ih = layer.img.height;
|
||||||
@@ -125,7 +127,7 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
|||||||
const url = ev.target?.result as string;
|
const url = ev.target?.result as string;
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
const nl: Layer = { id: `l-${Date.now()}`, url, img, x: CANVAS_SIZE / 2, y: CANVAS_SIZE / 2, scaleX: 1, scaleY: 1, rotation: 0, cropL: 0, cropT: 0, cropR: 0, cropB: 0, visible: true };
|
const nl: Layer = { id: `l-${Date.now()}`, url, img, x: canvasW / 2, y: canvasH / 2, scaleX: 1, scaleY: 1, rotation: 0, cropL: 0, cropT: 0, cropR: 0, cropB: 0, visible: true };
|
||||||
setLayers(p => [...p, nl]);
|
setLayers(p => [...p, nl]);
|
||||||
setSelectedId(nl.id);
|
setSelectedId(nl.id);
|
||||||
};
|
};
|
||||||
@@ -140,7 +142,7 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
|||||||
|
|
||||||
const getSF = () => {
|
const getSF = () => {
|
||||||
const r = containerRef.current?.getBoundingClientRect();
|
const r = containerRef.current?.getBoundingClientRect();
|
||||||
return r ? CANVAS_SIZE / r.width : 1;
|
return r ? canvasW / r.width : 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onHandleDown = (e: React.MouseEvent, handle: Handle, layer: Layer) => {
|
const onHandleDown = (e: React.MouseEvent, handle: Handle, layer: Layer) => {
|
||||||
@@ -166,13 +168,13 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
|||||||
let nx = d.layer.x + dx;
|
let nx = d.layer.x + dx;
|
||||||
let ny = d.layer.y + dy;
|
let ny = d.layer.y + dy;
|
||||||
// Clamp to canvas bounds with soft margin
|
// Clamp to canvas bounds with soft margin
|
||||||
nx = Math.max(0, Math.min(CANVAS_SIZE, nx));
|
nx = Math.max(0, Math.min(canvasW, nx));
|
||||||
ny = Math.max(0, Math.min(CANVAS_SIZE, ny));
|
ny = Math.max(0, Math.min(canvasH, ny));
|
||||||
// Hold Shift to snap to grid
|
// Hold Shift to snap to grid
|
||||||
if (isShift) { nx = snap(nx, SNAP_PX); ny = snap(ny, SNAP_PX); }
|
if (isShift) { nx = snap(nx, SNAP_PX); ny = snap(ny, SNAP_PX); }
|
||||||
// Center snapping: snap near canvas center
|
// Center snapping: snap near canvas center
|
||||||
if (Math.abs(nx - CANVAS_SIZE / 2) < SNAP_PX) nx = CANVAS_SIZE / 2;
|
if (Math.abs(nx - canvasW / 2) < SNAP_PX) nx = canvasW / 2;
|
||||||
if (Math.abs(ny - CANVAS_SIZE / 2) < SNAP_PX) ny = CANVAS_SIZE / 2;
|
if (Math.abs(ny - canvasH / 2) < SNAP_PX) ny = canvasH / 2;
|
||||||
updateLayer(d.layer.id, { x: nx, y: ny });
|
updateLayer(d.layer.id, { x: nx, y: ny });
|
||||||
} else if (d.handle === 'rotate') {
|
} else if (d.handle === 'rotate') {
|
||||||
let rot = d.layer.rotation + dx / 2;
|
let rot = d.layer.rotation + dx / 2;
|
||||||
@@ -228,11 +230,11 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
|||||||
const ih = (layer.img?.height ?? 200) * (1 - layer.cropT - layer.cropB);
|
const ih = (layer.img?.height ?? 200) * (1 - layer.cropT - layer.cropB);
|
||||||
const rect = containerRef.current?.getBoundingClientRect();
|
const rect = containerRef.current?.getBoundingClientRect();
|
||||||
if (!rect) return null;
|
if (!rect) return null;
|
||||||
const pct = (v: number) => `${(v / CANVAS_SIZE) * 100}%`;
|
|
||||||
const w = (iw * layer.scaleX / CANVAS_SIZE) * 100;
|
const w = (iw * layer.scaleX / canvasW) * 100;
|
||||||
const h = (ih * layer.scaleY / CANVAS_SIZE) * 100;
|
const h = (ih * layer.scaleY / canvasH) * 100;
|
||||||
const lx = (layer.x / CANVAS_SIZE) * 100;
|
const lx = (layer.x / canvasW) * 100;
|
||||||
const ly = (layer.y / CANVAS_SIZE) * 100;
|
const ly = (layer.y / canvasH) * 100;
|
||||||
|
|
||||||
const cornerStyle = (cx: string, cy: string, cursor: string): React.CSSProperties => ({
|
const cornerStyle = (cx: string, cy: string, cursor: string): React.CSSProperties => ({
|
||||||
position: 'absolute', left: cx, top: cy, width: 10, height: 10,
|
position: 'absolute', left: cx, top: cy, width: 10, height: 10,
|
||||||
@@ -411,7 +413,7 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
|||||||
onClick={() => setSelectedId(null)}>
|
onClick={() => setSelectedId(null)}>
|
||||||
<div ref={containerRef} className="relative shadow-2xl border border-zinc-700/50"
|
<div ref={containerRef} className="relative shadow-2xl border border-zinc-700/50"
|
||||||
style={{
|
style={{
|
||||||
width: 'min(68vh, 90%)', aspectRatio: '1/1',
|
width: 'min(68vh, 90%)', aspectRatio: `${canvasW}/${canvasH}`,
|
||||||
background: bgTransparent
|
background: bgTransparent
|
||||||
? 'repeating-conic-gradient(#27272a 0% 25%, #18181b 0% 50%) 0 0 / 16px 16px'
|
? 'repeating-conic-gradient(#27272a 0% 25%, #18181b 0% 50%) 0 0 / 16px 16px'
|
||||||
: bgColor
|
: bgColor
|
||||||
@@ -428,10 +430,10 @@ export function UVEditor({ onClose, onSave, targetObject, loadedModel, sceneObje
|
|||||||
onMouseDown={e => { e.stopPropagation(); onHandleDown(e, 'move', layer); }}
|
onMouseDown={e => { e.stopPropagation(); onHandleDown(e, 'move', layer); }}
|
||||||
className="absolute cursor-move"
|
className="absolute cursor-move"
|
||||||
style={{
|
style={{
|
||||||
left: `${(layer.x / CANVAS_SIZE) * 100}%`,
|
left: `${(layer.x / canvasW) * 100}%`,
|
||||||
top: `${(layer.y / CANVAS_SIZE) * 100}%`,
|
top: `${(layer.y / canvasH) * 100}%`,
|
||||||
width: `${(iw / CANVAS_SIZE) * 100}%`,
|
width: `${(iw / canvasW) * 100}%`,
|
||||||
height: `${(ih / CANVAS_SIZE) * 100}%`,
|
height: `${(ih / canvasH) * 100}%`,
|
||||||
transform: `translate(-50%,-50%) rotate(${layer.rotation}deg) scale(${layer.scaleX},${layer.scaleY})`,
|
transform: `translate(-50%,-50%) rotate(${layer.rotation}deg) scale(${layer.scaleX},${layer.scaleY})`,
|
||||||
zIndex: isSelected ? 4 : 2,
|
zIndex: isSelected ? 4 : 2,
|
||||||
transition: 'transform 0.04s linear',
|
transition: 'transform 0.04s linear',
|
||||||
|
|||||||
Reference in New Issue
Block a user