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 @@ -82,6 +82,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 };
})();
102 changes: 102 additions & 0 deletions scripts/leftPanelManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,108 @@ const LeftPanelManager = {
PanZoomManager.initPanning(stage);
PanZoomManager.initZooming(stage);

// Save/Load API
window.leftPanel = {
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();
}
}
};

return stage;
}
};
11 changes: 11 additions & 0 deletions scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ document.addEventListener('DOMContentLoaded', () => {
RightPanelManager.autoPackTextures(stageRight, false);
});

// 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 @@ -138,6 +142,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
23 changes: 21 additions & 2 deletions scripts/polygonManager.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
// ==================== POLYGON MANAGEMENT ====================
const PolygonManager = {
// Unified polygon creation function
createPolygonGroup: (stage, layer, points = null, dirtyPolygons = null) => {
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,
name: 'group',
Expand All @@ -15,7 +34,7 @@ const PolygonManager = {
let vertices;
if (points && points.length === 4) {
// REORDER vertices to ensure consistent order for both polygon types
vertices = Utils.reorderPolygonVertices(points);
vertices = skipReorder ? points.map(p => ({x: p.x, y: p.y})) : Utils.reorderPolygonVertices(points);
} else {
// Create default rectangle centered on stage
const stageCenterX = stage.width() / 2;
Expand Down
Loading