Skip to content
Open
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
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
<script src="scripts/dragAndDropManager.js"></script>
<script src="scripts/leftPanelManager.js"></script>
<script src="scripts/rightPanelManager.js"></script>
<script src="scripts/saveManager.js"></script>
<script src="scripts/main.js"></script>
</body>
</html>
37 changes: 36 additions & 1 deletion main.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { app, BrowserWindow, dialog, ipcMain } = require('electron');
const { app, BrowserWindow, dialog, ipcMain, Menu } = require('electron');
const fs = require('fs');
const { autoUpdater } = require('electron-updater');
const path = require('path');

Expand All @@ -17,6 +18,22 @@ function createWindow() {
mainWindow.maximize();
mainWindow.loadFile('index.html');

const menuTemplate = [
{
label: 'File',
submenu: [
{ label: 'Save Project', accelerator: 'CmdOrCtrl+S', click: () => mainWindow.webContents.send('menu-save-project') },
{ label: 'Open Project', accelerator: 'CmdOrCtrl+O', click: () => mainWindow.webContents.send('menu-open-project') },
{ type: 'separator' },
{ role: 'quit' }
]
},
{ role: 'editMenu' },
{ role: 'viewMenu' },
{ role: 'windowMenu' }
];
Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate));

// Check for updates after window is ready
mainWindow.webContents.once('did-finish-load', () => {
console.log('Window loaded, checking for updates...');
Expand Down Expand Up @@ -107,6 +124,24 @@ ipcMain.handle('get-app-version', () => {
return app.getVersion();
});

ipcMain.handle('save-project-dialog', async () => {
const { filePath } = await dialog.showSaveDialog(mainWindow, {
title: 'Save Project', defaultPath: 'project.trp',
filters: [{ name: 'Texture Ripper Project', extensions: ['trp'] }]
});
return filePath || null;
});

ipcMain.handle('open-project-dialog', async () => {
const { filePaths } = await dialog.showOpenDialog(mainWindow, {
title: 'Open Project',
filters: [{ name: 'Texture Ripper Project', extensions: ['trp'] }],
properties: ['openFile']
});
if (filePaths && filePaths.length > 0) return filePaths[0];
return null;
});

app.on('ready', createWindow);

app.on('window-all-closed', () => {
Expand Down
73 changes: 71 additions & 2 deletions scripts/feedbackManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const FeedbackManager = (() => {
let feedbackEl = null;
let spinnerOverlay = null;

function init() {
if (!feedbackEl) {
Expand All @@ -15,7 +16,7 @@ const FeedbackManager = (() => {
background: 'rgba(0,0,0,0.85)',
color: '#fff',
borderRadius: '6px',
zIndex: 9999,
zIndex: 10000,
fontFamily: 'sans-serif',
fontSize: '16px',
pointerEvents: 'none',
Expand All @@ -29,6 +30,62 @@ const FeedbackManager = (() => {
}
}

function initSpinner() {
if (!spinnerOverlay) {
spinnerOverlay = document.createElement('div');
Object.assign(spinnerOverlay.style, {
position: 'fixed',
top: '0', left: '0', width: '100%', height: '100%',
background: 'rgba(0,0,0,0.5)',
display: 'none',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999
});

const box = document.createElement('div');
Object.assign(box.style, {
background: 'rgba(0,0,0,0.85)',
borderRadius: '8px',
padding: '24px 32px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '12px'
});

const spinner = document.createElement('div');
Object.assign(spinner.style, {
width: '32px', height: '32px',
border: '3px solid rgba(255,255,255,0.2)',
borderTop: '3px solid #fff',
borderRadius: '50%',
animation: 'feedback-spin 0.8s linear infinite'
});

const label = document.createElement('div');
label.className = 'spinner-label';
Object.assign(label.style, {
color: '#fff',
fontFamily: 'sans-serif',
fontSize: '14px'
});

// Add keyframes
if (!document.getElementById('feedback-spin-style')) {
const style = document.createElement('style');
style.id = 'feedback-spin-style';
style.textContent = '@keyframes feedback-spin { to { transform: rotate(360deg); } }';
document.head.appendChild(style);
}

box.appendChild(spinner);
box.appendChild(label);
spinnerOverlay.appendChild(box);
document.body.appendChild(spinnerOverlay);
}
}

function show(message, options = {}) {
init();

Expand All @@ -47,5 +104,17 @@ const FeedbackManager = (() => {
}, duration);
}

return { show };
function showSpinner(message = 'Please wait...') {
initSpinner();
spinnerOverlay.querySelector('.spinner-label').textContent = message;
spinnerOverlay.style.display = 'flex';
}

function hideSpinner() {
if (spinnerOverlay) {
spinnerOverlay.style.display = 'none';
}
}

return { show, showSpinner, hideSpinner };
})();
100 changes: 100 additions & 0 deletions scripts/leftPanelManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ const LeftPanelManager = {
PanZoomManager.initPanning(stage);
PanZoomManager.initZooming(stage);

// Save/Load API
window.leftPanel = {
autoPackImages: () => {
if (bgImages.length === 0) return;
Expand Down Expand Up @@ -605,6 +606,105 @@ const LeftPanelManager = {

bgLayer.batchDraw();
FeedbackManager.show('Arranged ' + bgImages.length + ' image(s)');
},

getState: () => {
const images = bgImages.map(img => ({
dataURL: SaveManager.imageToDataURL(img, 'image/jpeg', 0.92),
x: img.x(),
y: img.y(),
width: img.width(),
height: img.height(),
scaleX: img.scaleX(),
scaleY: img.scaleY(),
rotation: img.rotation()
}));

const polygons = [];
polygonLayer.find('.group').forEach(group => {
const verts = group.vertices.map(v => ({ x: v.x, y: v.y }));
const mids = group.midpoints.map(m => ({ x: m.x, y: m.y, locked: m.locked }));
polygons.push({
id: group._id,
x: group.x(),
y: group.y(),
vertices: verts,
midpoints: mids
});
});

return { images, polygons };
},

loadState: (state) => {
// Clear existing
bgImages.forEach(img => img.destroy());
bgImages.length = 0;
polygonLayer.find('.group').forEach(g => g.destroy());
bgLayer.batchDraw();
polygonLayer.batchDraw();

// Restore images
if (state.images) {
state.images.forEach(imgData => {
const img = new Image();
img.onload = () => {
const konvaImg = new Konva.Image({
x: imgData.x,
y: imgData.y,
image: img,
width: imgData.width,
height: imgData.height,
scaleX: imgData.scaleX || 1,
scaleY: imgData.scaleY || 1,
rotation: imgData.rotation || 0,
draggable: !imagesLocked
});
bgLayer.add(konvaImg);
bgImages.push(konvaImg);
bgLayer.batchDraw();
};
img.src = imgData.dataURL;
});
}

// Restore polygons
if (state.polygons) {
state.polygons.forEach(polyData => {
const group = PolygonManager.createPolygonGroup(
stage, polygonLayer, polyData.vertices, dirtyPolygons, true
);
group.position({ x: polyData.x || 0, y: polyData.y || 0 });

// Restore midpoints
if (polyData.midpoints) {
polyData.midpoints.forEach((m, i) => {
if (group.midpoints[i]) {
group.midpoints[i].x = m.x;
group.midpoints[i].y = m.y;
group.midpoints[i].locked = m.locked || false;
}
});
// Update visual midpoint positions
group.find('.midpoint').forEach((mp, i) => {
if (group.midpoints[i]) {
mp.position({ x: group.midpoints[i].x, y: group.midpoints[i].y });
}
});
// Redraw polygon and grid with restored midpoints
PolygonManager.drawCurvedPolygon(group, group.vertices, group.midpoints);
GridManager.drawGrid(group, group.vertices, group.midpoints);
const updatedPoints = PolygonManager.computeDragSurfacePoints(group.vertices, group.midpoints);
PolygonManager.updateDragSurface(group, updatedPoints);
}

// Reassign ID if saved
if (polyData.id) group._id = polyData.id;

dirtyPolygons.add(group._id);
});
polygonLayer.batchDraw();
}
}
};

Expand Down
11 changes: 11 additions & 0 deletions scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ document.addEventListener('DOMContentLoaded', () => {
if (window.leftPanel) UndoManager.redo();
});

// Save/Load project functions
window.saveProject = () => SaveManager.save(stageLeft, stageRight);
window.loadProject = () => SaveManager.load(stageLeft, stageRight);

// Export button
document.getElementById('exportRight').addEventListener('click', () => {
const exportWidth = parseInt(document.getElementById('rightWidth').value);
Expand Down Expand Up @@ -161,6 +165,13 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('requestFeature').addEventListener('click', () => {
openExternalURL('https://github.com/raycastly/texture-ripper/issues/new?template=feature_request.yml');
});

// Listen for Electron menu events (File > Save/Open)
if (isElectron()) {
const { ipcRenderer } = require('electron');
ipcRenderer.on('menu-save-project', () => window.saveProject());
ipcRenderer.on('menu-open-project', () => window.loadProject());
}
});


Expand Down
19 changes: 19 additions & 0 deletions scripts/polygonManager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
// ==================== POLYGON MANAGEMENT ====================
const PolygonManager = {
// Unified polygon creation function
computeDragSurfacePoints: (vertices, midpoints) => {
const points = [];
for (let i = 0; i < vertices.length; i++) {
const nextIdx = (i + 1) % vertices.length;
const P0 = vertices[i];
const P2 = vertices[nextIdx];
const M = midpoints[i];
const numSamples = 10;
for (let j = 0; j <= numSamples; j++) {
const t = j / numSamples;
const mt = 1 - t;
const x = mt*mt*P0.x + 2*mt*t*M.x + t*t*P2.x;
const y = mt*mt*P0.y + 2*mt*t*M.y + t*t*P2.y;
points.push(x, y);
}
}
return points;
},

createPolygonGroup: (stage, layer, points = null, dirtyPolygons = null, skipReorder = false) => {
const group = new Konva.Group({
draggable: true,
Expand Down
64 changes: 64 additions & 0 deletions scripts/rightPanelManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,70 @@ const RightPanelManager = {
}
},

getState: () => {
const textures = [];
Object.keys(tiedRects).forEach(groupId => {
const img = tiedRects[groupId];
if (img) {
textures.push({
groupId: groupId,
dataURL: SaveManager.imageToDataURL(img),
x: img.x(),
y: img.y(),
width: img.width(),
height: img.height(),
scaleX: img.scaleX(),
scaleY: img.scaleY(),
rotation: img.rotation()
});
}
});
return { textures };
},

loadState: (state) => {
// Clear existing textures
Object.keys(tiedRects).forEach(id => {
if (tiedRects[id]) tiedRects[id].destroy();
delete tiedRects[id];
});
imageLayer.batchDraw();

console.log(`Loading ${state.textures ? state.textures.length : 0} textures`);
if (state.textures) {
state.textures.forEach((texData, i) => {
console.log(`Texture ${i}: groupId=${texData.groupId}, hasDataURL=${!!texData.dataURL}, dataURLLen=${texData.dataURL ? texData.dataURL.length : 0}, w=${texData.width}, h=${texData.height}`);
const img = new Image();
img.onload = () => {
console.log(`Texture ${i} image loaded: ${img.naturalWidth}x${img.naturalHeight}`);
const konvaImg = new Konva.Image({
x: texData.x,
y: texData.y,
image: img,
width: texData.width || img.naturalWidth,
height: texData.height || img.naturalHeight,
scaleX: texData.scaleX || 1,
scaleY: texData.scaleY || 1,
rotation: texData.rotation || 0,
id: `rect_${texData.groupId}`,
draggable: true
});

konvaImg.on('dragmove', snapping.handleDragging);
konvaImg.on('dragend', snapping.handleDragEnd);

imageLayer.add(konvaImg);
tiedRects[texData.groupId] = konvaImg;
imageLayer.batchDraw();
};
img.onerror = (err) => {
console.error(`Texture ${i} failed to load:`, err);
};
img.src = texData.dataURL;
});
}
},

updateBackground: () => {
const width = parseInt(document.getElementById('rightWidth').value);
const height = parseInt(document.getElementById('rightHeight').value);
Expand Down
Loading