Skip to content

Commit 89fc47f

Browse files
committed
v1.1.0: Z-up 3D view, fix safety factor calculation, improve failure mode labels
1 parent 49c125e commit 89fc47f

File tree

14 files changed

+725
-221
lines changed

14 files changed

+725
-221
lines changed

electron/main.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,23 @@ ipcMain.on('window:close', () => mainWindow?.close())
160160
ipcMain.handle('window:is-maximized', () => mainWindow?.isMaximized())
161161

162162
// File dialogs
163-
ipcMain.handle('dialog:save-config', async (_, data: string) => {
163+
// Save to existing path (for Save and autosave)
164+
ipcMain.handle('dialog:save-to-path', async (_, data: string, filePath: string) => {
165+
try {
166+
fs.writeFileSync(filePath, data, 'utf-8')
167+
return { success: true, filePath }
168+
} catch (err) {
169+
return { success: false, error: String(err) }
170+
}
171+
})
172+
173+
// Save with dialog (for Save As... or first-time Save)
174+
ipcMain.handle('dialog:save-config', async (_, data: string, defaultName?: string) => {
164175
const result = await dialog.showSaveDialog(mainWindow!, {
165176
title: 'Save Configuration',
166-
defaultPath: 'buoyancy-config.json',
177+
defaultPath: defaultName || 'config.tube',
167178
filters: [
179+
{ name: 'Tube Files', extensions: ['tube'] },
168180
{ name: 'JSON Files', extensions: ['json'] },
169181
{ name: 'All Files', extensions: ['*'] }
170182
]
@@ -185,6 +197,7 @@ ipcMain.handle('dialog:load-config', async () => {
185197
const result = await dialog.showOpenDialog(mainWindow!, {
186198
title: 'Load Configuration',
187199
filters: [
200+
{ name: 'Tube Files', extensions: ['tube'] },
188201
{ name: 'JSON Files', extensions: ['json'] },
189202
{ name: 'All Files', extensions: ['*'] }
190203
],
@@ -214,7 +227,7 @@ ipcMain.handle('dialog:load-recent-file', async (_, filePath: string) => {
214227
ipcMain.handle('dialog:export-results', async (_, data: string) => {
215228
const result = await dialog.showSaveDialog(mainWindow!, {
216229
title: 'Export Results',
217-
defaultPath: 'buoyancy-results.csv',
230+
defaultPath: 'tubes-results.csv',
218231
filters: [
219232
{ name: 'CSV Files', extensions: ['csv'] },
220233
{ name: 'JSON Files', extensions: ['json'] },

electron/preload.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
2525
isMaximized: () => ipcRenderer.invoke('window:is-maximized'),
2626

2727
// File operations
28-
saveConfig: (data: string) => ipcRenderer.invoke('dialog:save-config', data),
28+
saveToPath: (data: string, filePath: string) => ipcRenderer.invoke('dialog:save-to-path', data, filePath),
29+
saveConfig: (data: string, defaultName?: string) => ipcRenderer.invoke('dialog:save-config', data, defaultName),
2930
loadConfig: () => ipcRenderer.invoke('dialog:load-config'),
3031
loadRecentFile: (filePath: string) => ipcRenderer.invoke('dialog:load-recent-file', filePath),
3132
exportResults: (data: string) => ipcRenderer.invoke('dialog:export-results', data),
@@ -60,7 +61,8 @@ declare global {
6061
maximize: () => void
6162
close: () => void
6263
isMaximized: () => Promise<boolean>
63-
saveConfig: (data: string) => Promise<SaveResult>
64+
saveToPath: (data: string, filePath: string) => Promise<SaveResult>
65+
saveConfig: (data: string, defaultName?: string) => Promise<SaveResult>
6466
loadConfig: () => Promise<LoadResult>
6567
loadRecentFile: (filePath: string) => Promise<LoadResult>
6668
exportResults: (data: string) => Promise<SaveResult>

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<meta name="color-scheme" content="dark" />
7-
<title>Buoyancy Optimizer</title>
7+
<title>Everything We Currently Know About Tubes</title>
88
<style>
99
html, body {
1010
background-color: #1e1e1e;

package.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
{
2-
"name": "buoyancy-optimizer",
3-
"version": "1.0.0",
4-
"description": "Buoyancy Cylinder Optimizer - Optimize pressure vessel dimensions for underwater applications",
5-
"type": "module",
2+
"name": "everything-we-currently-know-about-tubes",
3+
"version": "1.1.0",
4+
"description": "Everything We Currently Know About Tubes - Optimize pressure vessel dimensions for underwater applications",
65
"main": "dist-electron/main.js",
76
"scripts": {
87
"dev": "vite",
@@ -37,8 +36,8 @@
3736
"vite-plugin-electron-renderer": "^0.14.6"
3837
},
3938
"build": {
40-
"appId": "com.buoyancy.optimizer",
41-
"productName": "Buoyancy Optimizer",
39+
"appId": "com.tubes.everything",
40+
"productName": "Everything We Currently Know About Tubes",
4241
"directories": {
4342
"output": "release"
4443
},

src/App.tsx

Lines changed: 112 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react'
1+
import { useEffect, useState, useRef, useCallback } from 'react'
22
import { Play } from 'lucide-react'
33
import { useAppStore, useProjectConfig, useProjectResults, SavedProject } from './stores/appStore'
44
import { optimize, depthToPressure } from './lib/optimizer'
@@ -26,7 +26,10 @@ function App() {
2626
getActiveProject,
2727
newProject,
2828
addRecentFile,
29-
getSelectedMaterialKeys
29+
getSelectedMaterialKeys,
30+
autoSaveEnabled,
31+
projects,
32+
activeProjectId
3033
} = useAppStore()
3134

3235
const config = useProjectConfig()
@@ -35,6 +38,7 @@ function App() {
3538
const [isResizing, setIsResizing] = useState(false)
3639
const [statusMessage, setStatusMessage] = useState('')
3740
const [viewerWidth, setViewerWidth] = useState(450) // Default wider viewer
41+
const autoSaveTimerRef = useRef<number | null>(null)
3842

3943
// Handle sidebar resize
4044
useEffect(() => {
@@ -64,14 +68,15 @@ function App() {
6468
}
6569
}, [isResizing, setSidebarWidth])
6670

67-
// Save config handler
68-
const handleSaveConfig = async () => {
71+
// Save config handler - saves to existing path if available
72+
const handleSaveConfig = useCallback(async () => {
73+
const project = getActiveProject()
6974
const projectData = exportProject()
7075
if (!projectData) return
7176

7277
if (!window.electronAPI) {
7378
// Fallback for browser - download as file
74-
const fileName = `${projectData.name.replace(/[^a-z0-9]/gi, '-')}.buoy.json`
79+
const fileName = `${projectData.name.replace(/[^a-z0-9]/gi, '-')}.tube`
7580
const blob = new Blob([JSON.stringify(projectData, null, 2)], { type: 'application/json' })
7681
const url = URL.createObjectURL(blob)
7782
const a = document.createElement('a')
@@ -85,21 +90,83 @@ function App() {
8590
return
8691
}
8792

88-
const result = await window.electronAPI.saveConfig(JSON.stringify(projectData, null, 2))
93+
// If project has existing path, save directly to that path
94+
if (project?.filePath) {
95+
const result = await window.electronAPI.saveToPath(JSON.stringify(projectData, null, 2), project.filePath)
96+
if (result.success) {
97+
markProjectSaved(project.filePath)
98+
setStatusMessage(`Saved to ${project.filePath}`)
99+
setTimeout(() => setStatusMessage(''), 3000)
100+
}
101+
} else {
102+
// Otherwise show save dialog
103+
const result = await window.electronAPI.saveConfig(JSON.stringify(projectData, null, 2), `${projectData.name}.tube`)
104+
if (result.success && result.filePath) {
105+
markProjectSaved(result.filePath)
106+
setStatusMessage(`Saved to ${result.filePath}`)
107+
setTimeout(() => setStatusMessage(''), 3000)
108+
}
109+
}
110+
}, [exportProject, markProjectSaved, getActiveProject])
111+
112+
// Save As... handler - always shows dialog
113+
const handleSaveConfigAs = useCallback(async () => {
114+
const projectData = exportProject()
115+
if (!projectData) return
116+
117+
if (!window.electronAPI) {
118+
// Fallback for browser - same as regular save
119+
const fileName = `${projectData.name.replace(/[^a-z0-9]/gi, '-')}.tube`
120+
const blob = new Blob([JSON.stringify(projectData, null, 2)], { type: 'application/json' })
121+
const url = URL.createObjectURL(blob)
122+
const a = document.createElement('a')
123+
a.href = url
124+
a.download = fileName
125+
a.click()
126+
URL.revokeObjectURL(url)
127+
markProjectSaved(fileName)
128+
setStatusMessage('Project downloaded')
129+
setTimeout(() => setStatusMessage(''), 3000)
130+
return
131+
}
132+
133+
const result = await window.electronAPI.saveConfig(JSON.stringify(projectData, null, 2), `${projectData.name}.tube`)
89134
if (result.success && result.filePath) {
90135
markProjectSaved(result.filePath)
91136
setStatusMessage(`Saved to ${result.filePath}`)
92137
setTimeout(() => setStatusMessage(''), 3000)
93138
}
94-
}
139+
}, [exportProject, markProjectSaved])
140+
141+
// Autosave - only saves if project has existing filePath (has been saved before)
142+
const autoSave = useCallback(async () => {
143+
const project = getActiveProject()
144+
if (!project || !project.modified || !project.filePath) return
145+
if (!window.electronAPI) return
146+
147+
const projectData = exportProject()
148+
if (!projectData) return
149+
150+
// Save directly to existing path
151+
try {
152+
const result = await window.electronAPI.saveToPath(JSON.stringify(projectData, null, 2), project.filePath)
153+
if (result.success) {
154+
markProjectSaved(project.filePath)
155+
setStatusMessage('Autosaved')
156+
setTimeout(() => setStatusMessage(''), 2000)
157+
}
158+
} catch {
159+
// Silent fail for autosave
160+
}
161+
}, [getActiveProject, exportProject, markProjectSaved])
95162

96163
// Load config handler
97164
const handleLoadConfig = async () => {
98165
if (!window.electronAPI) {
99166
// Fallback for browser - use file input
100167
const input = document.createElement('input')
101168
input.type = 'file'
102-
input.accept = '.json,.buoy.json'
169+
input.accept = '.tube,.json'
103170
input.onchange = async (e) => {
104171
const file = (e.target as HTMLInputElement).files?.[0]
105172
if (file) {
@@ -191,7 +258,7 @@ function App() {
191258
const url = URL.createObjectURL(blob)
192259
const a = document.createElement('a')
193260
a.href = url
194-
a.download = 'buoyancy-results.csv'
261+
a.download = 'tubes-results.csv'
195262
a.click()
196263
URL.revokeObjectURL(url)
197264
setStatusMessage('Results downloaded')
@@ -230,6 +297,30 @@ function App() {
230297
return cleanup
231298
}, [toggleSidebar])
232299

300+
// Autosave effect - debounced save when project is modified
301+
useEffect(() => {
302+
const project = projects.find(p => p.id === activeProjectId)
303+
if (!autoSaveEnabled || !project?.modified || !project?.filePath) {
304+
return
305+
}
306+
307+
// Clear any existing timer
308+
if (autoSaveTimerRef.current) {
309+
clearTimeout(autoSaveTimerRef.current)
310+
}
311+
312+
// Set a new timer for autosave (2 second debounce)
313+
autoSaveTimerRef.current = window.setTimeout(() => {
314+
autoSave()
315+
}, 2000)
316+
317+
return () => {
318+
if (autoSaveTimerRef.current) {
319+
clearTimeout(autoSaveTimerRef.current)
320+
}
321+
}
322+
}, [autoSaveEnabled, projects, activeProjectId, autoSave])
323+
233324
// Keyboard shortcuts
234325
useEffect(() => {
235326
const handleKeyDown = (e: KeyboardEvent) => {
@@ -245,7 +336,11 @@ function App() {
245336
break
246337
case 's':
247338
e.preventDefault()
248-
handleSaveConfig()
339+
if (e.shiftKey) {
340+
handleSaveConfigAs()
341+
} else {
342+
handleSaveConfig()
343+
}
249344
break
250345
case 'e':
251346
e.preventDefault()
@@ -261,7 +356,7 @@ function App() {
261356

262357
window.addEventListener('keydown', handleKeyDown)
263358
return () => window.removeEventListener('keydown', handleKeyDown)
264-
}, [toggleSidebar, results])
359+
}, [toggleSidebar, results, handleSaveConfig, handleSaveConfigAs, newProject])
265360

266361
// Run optimization
267362
const runOptimization = () => {
@@ -287,7 +382,10 @@ function App() {
287382
diameterStepMm: config.diameterStepMm,
288383
lengthStepMm: config.lengthStepMm,
289384
waterDensity: config.waterDensity,
290-
box: config.box
385+
box: config.box,
386+
forcedWallThicknessMm: config.forcedWallThicknessMm,
387+
forcedEndcapThicknessMm: config.forcedEndcapThicknessMm,
388+
endcapConstraint: config.endcapConstraint
291389
})
292390

293391
const selectedCount = getSelectedMaterialKeys().length
@@ -304,6 +402,7 @@ function App() {
304402
onNew={newProject}
305403
onOpen={handleLoadConfig}
306404
onSave={handleSaveConfig}
405+
onSaveAs={handleSaveConfigAs}
307406
onExport={handleExportResults}
308407
onToggleSidebar={toggleSidebar}
309408
onOpenRecent={handleOpenRecent}
@@ -330,7 +429,7 @@ function App() {
330429
{/* Toolbar */}
331430
<div className="h-10 bg-vsc-bg-dark border-b border-vsc-border flex items-center justify-between px-4 flex-shrink-0">
332431
<div className="text-sm text-vsc-fg-dim">
333-
{statusMessage || 'Optimize cylinder dimensions for maximum buoyancy'}
432+
{statusMessage || 'Everything we currently know about tubes'}
334433
</div>
335434
<button
336435
onClick={runOptimization}

0 commit comments

Comments
 (0)