diff --git a/package-lock.json b/package-lock.json index 3e83d61..3a8c589 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "surface-expert", - "version": "2.9.6", + "version": "2.9.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "surface-expert", - "version": "2.9.6", + "version": "2.9.7", "license": "MIT", "dependencies": { "react": "^18.2.0", diff --git a/package.json b/package.json index e1d6ed7..f17a81d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "surface-expert", - "version": "2.9.6", + "version": "2.9.7", "description": "SurfaceExpert - Optical Surface Analysis Desktop Application", "main": "src/main.js", "scripts": { diff --git a/src/components/panels/PropertiesPanel.js b/src/components/panels/PropertiesPanel.js index b146480..0f203fe 100644 --- a/src/components/panels/PropertiesPanel.js +++ b/src/components/panels/PropertiesPanel.js @@ -32,6 +32,10 @@ export const PropertiesPanel = ({ handleNormalizeUnZ, handleConvertToUnZ, handleConvertToPoly, + handleFlipX, + handleFlipY, + handleFlipZ, + handleCopyCoefficients, c, t }) => { @@ -383,6 +387,10 @@ export const PropertiesPanel = ({ onNormalizeUnZ: handleNormalizeUnZ, onConvertToUnZ: handleConvertToUnZ, onConvertToPoly: handleConvertToPoly, + onFlipX: handleFlipX, + onFlipY: handleFlipY, + onFlipZ: handleFlipZ, + onCopyCoefficients: handleCopyCoefficients, c, t }), diff --git a/src/components/ui/SurfaceActionButtons.js b/src/components/ui/SurfaceActionButtons.js index e3f3511..0d17be9 100644 --- a/src/components/ui/SurfaceActionButtons.js +++ b/src/components/ui/SurfaceActionButtons.js @@ -13,7 +13,7 @@ const { createElement: h } = React; * @param {Function} props.onConvertToPoly - Callback for UnZ → Poly conversion * @param {Object} props.c - Color scheme object */ -export const SurfaceActionButtons = ({ surface, onInvert, onNormalizeUnZ, onConvertToUnZ, onConvertToPoly, c, t }) => { +export const SurfaceActionButtons = ({ surface, onInvert, onNormalizeUnZ, onConvertToUnZ, onConvertToPoly, onFlipX, onFlipY, onFlipZ, onCopyCoefficients, c, t }) => { const buttonStyle = { padding: '8px 16px', backgroundColor: c.accent, @@ -69,7 +69,40 @@ export const SurfaceActionButtons = ({ surface, onInvert, onNormalizeUnZ, onConv title: 'Convert this Poly surface to Opal Un Z' }, t.properties.convertToUnZ), - // Convert to Poly button - shown only for Opal Un Z + // Flip and Copy buttons - shown only for Zernike + surface.type === 'Zernike' && h('button', { + onClick: onFlipX, + style: buttonStyle, + onMouseEnter: (e) => e.target.style.backgroundColor = '#3a7bc8', + onMouseLeave: (e) => e.target.style.backgroundColor = c.accent, + title: 'Create a new surface with Zernike coefficients mirrored about the X-axis (y → -y)' + }, 'Flip around X'), + + surface.type === 'Zernike' && h('button', { + onClick: onFlipY, + style: buttonStyle, + onMouseEnter: (e) => e.target.style.backgroundColor = '#3a7bc8', + onMouseLeave: (e) => e.target.style.backgroundColor = c.accent, + title: 'Create a new surface with Zernike coefficients mirrored about the Y-axis (x → -x)' + }, 'Flip around Y'), + + surface.type === 'Zernike' && h('button', { + onClick: onFlipZ, + style: buttonStyle, + onMouseEnter: (e) => e.target.style.backgroundColor = '#3a7bc8', + onMouseLeave: (e) => e.target.style.backgroundColor = c.accent, + title: 'Create a new surface with Zernike coefficients rotated 180° about the Z-axis (x → -x, y → -y)' + }, 'Flip around Z'), + + surface.type === 'Zernike' && h('button', { + onClick: onCopyCoefficients, + style: buttonStyle, + onMouseEnter: (e) => e.target.style.backgroundColor = '#3a7bc8', + onMouseLeave: (e) => e.target.style.backgroundColor = c.accent, + title: 'Copy Z1-Z37 values as tab-separated text for pasting into Excel' + }, 'Copy Coefficients'), + + // Convert to Poly button - shown only for Opal Un Z surface.type === 'Opal Un Z' && h('button', { onClick: onConvertToPoly, style: buttonStyle, diff --git a/src/renderer-modular.js b/src/renderer-modular.js index 1048821..d00999d 100644 --- a/src/renderer-modular.js +++ b/src/renderer-modular.js @@ -30,7 +30,11 @@ import { handleNormalizeUnZConfirm as normalizeUnZConfirmHandler, handleConvertToUnZ as convertToUnZHandler, handleConvertToPoly as convertToPolyHandler, - handleFastConvertToPoly as fastConvertToPolyHandler + handleFastConvertToPoly as fastConvertToPolyHandler, + handleFlipZernikeX as flipZernikeXHandler, + handleFlipZernikeY as flipZernikeYHandler, + handleFlipZernikeZ as flipZernikeZHandler, + handleCopyZernikeCoefficients as copyZernikeCoefficientsHandler } from './utils/surfaceOperationHandlers.js'; import { parseNumber } from './utils/numberParsing.js'; @@ -427,6 +431,22 @@ const OpticalSurfaceAnalyzer = () => { convertToPolyHandler(selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface); }; + const handleFlipX = () => { + flipZernikeXHandler(selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface); + }; + + const handleFlipY = () => { + flipZernikeYHandler(selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface); + }; + + const handleFlipZ = () => { + flipZernikeZHandler(selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface); + }; + + const handleCopyCoefficients = () => { + copyZernikeCoefficientsHandler(selectedSurface); + }; + const handleFastConvertToPoly = () => { fastConvertToPolyHandler( selectedSurface, @@ -1093,6 +1113,10 @@ const OpticalSurfaceAnalyzer = () => { handleNormalizeUnZ, handleConvertToUnZ, handleConvertToPoly, + handleFlipX, + handleFlipY, + handleFlipZ, + handleCopyCoefficients, c, t }) diff --git a/src/utils/surfaceOperationHandlers.js b/src/utils/surfaceOperationHandlers.js index 45125ad..ab8f76d 100644 --- a/src/utils/surfaceOperationHandlers.js +++ b/src/utils/surfaceOperationHandlers.js @@ -3,7 +3,7 @@ // ============================================ // Business logic for surface transformation operations -import { normalizeUnZ, convertPolyToUnZ, convertUnZToPoly, invertSurface } from './surfaceTransformations.js'; +import { normalizeUnZ, convertPolyToUnZ, convertUnZToPoly, invertSurface, flipZernikeAroundX, flipZernikeAroundY, flipZernikeAroundZ } from './surfaceTransformations.js'; import { parseNumber } from './numberParsing.js'; import { calculateSurfaceValues } from './calculations.js'; @@ -387,3 +387,101 @@ function calculateMaxDeviation(deviations) { return maxDev; } + +// ============================================ +// Zernike Flip Handlers +// ============================================ + +const addFlippedZernikeSurface = (selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface, flippedParams, suffix) => { + const newSurface = { + id: Date.now(), + name: `${selectedSurface.name} (${suffix})`, + type: 'Zernike', + color: selectedSurface.color, + parameters: flippedParams + }; + + const updatedFolders = folders.map(folder => { + if (folder.id === selectedFolder.id) { + return { ...folder, surfaces: [...folder.surfaces, newSurface] }; + } + return folder; + }); + + setFolders(updatedFolders); + setSelectedSurface(newSurface); + + if (window.electronAPI && window.electronAPI.saveSurface) { + window.electronAPI.saveSurface(selectedFolder.name, newSurface); + } +}; + +/** + * Flip Zernike surface around X-axis (y → -y) and create a new surface. + */ +export const handleFlipZernikeX = (selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface) => { + if (!selectedSurface || selectedSurface.type !== 'Zernike' || !selectedFolder) return; + try { + const flipped = flipZernikeAroundX(selectedSurface.parameters); + addFlippedZernikeSurface(selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface, flipped, 'flipped around X'); + } catch (error) { + alert(`Error flipping surface around X: ${error.message}`); + console.error('Flip X error:', error); + } +}; + +/** + * Flip Zernike surface around Y-axis (x → -x) and create a new surface. + */ +export const handleFlipZernikeY = (selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface) => { + if (!selectedSurface || selectedSurface.type !== 'Zernike' || !selectedFolder) return; + try { + const flipped = flipZernikeAroundY(selectedSurface.parameters); + addFlippedZernikeSurface(selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface, flipped, 'flipped around Y'); + } catch (error) { + alert(`Error flipping surface around Y: ${error.message}`); + console.error('Flip Y error:', error); + } +}; + +/** + * Flip Zernike surface around Z-axis (x → -x, y → -y) and create a new surface. + */ +export const handleFlipZernikeZ = (selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface) => { + if (!selectedSurface || selectedSurface.type !== 'Zernike' || !selectedFolder) return; + try { + const flipped = flipZernikeAroundZ(selectedSurface.parameters); + addFlippedZernikeSurface(selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface, flipped, 'flipped around Z'); + } catch (error) { + alert(`Error flipping surface around Z: ${error.message}`); + console.error('Flip Z error:', error); + } +}; + +/** + * Copy all Zernike coefficients Z1-Z37 to clipboard as tab-separated values. + */ +export const handleCopyZernikeCoefficients = (selectedSurface) => { + if (!selectedSurface || selectedSurface.type !== 'Zernike') return; + const values = Array.from({ length: 37 }, (_, i) => { + const key = `Z${i + 1}`; + return selectedSurface.parameters[key] || '0'; + }); + const text = values.join('\t'); + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).catch(() => fallbackCopy(text)); + } else { + fallbackCopy(text); + } +}; + +function fallbackCopy(text) { + const el = document.createElement('textarea'); + el.value = text; + el.style.position = 'fixed'; + el.style.opacity = '0'; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); +} diff --git a/src/utils/surfaceTransformations.js b/src/utils/surfaceTransformations.js index bfeba8c..d8409fc 100644 --- a/src/utils/surfaceTransformations.js +++ b/src/utils/surfaceTransformations.js @@ -237,3 +237,71 @@ export const invertSurface = (surfaceType, parameters) => { return result; }; + +/** + * Zernike azimuthal symmetry map: + * m=0 (spherical): Z1,Z4,Z9,Z16,Z25,Z36,Z37 + * m=1 cos(X): Z2,Z7,Z14,Z23,Z34 sin(Y): Z3,Z8,Z15,Z24,Z35 + * m=2 cos(X): Z5,Z12,Z21,Z32 sin(Y): Z6,Z13,Z22,Z33 + * m=3 cos(X): Z10,Z19,Z30 sin(Y): Z11,Z20,Z31 + * m=4 cos(X): Z17,Z28 sin(Y): Z18,Z29 + * m=5 cos(X): Z26 sin(Y): Z27 + */ + +/** + * Flip Zernike surface around X-axis (y → -y, θ → -θ). + * Negates all sine (Y) azimuthal terms; cosine (X) and spherical terms unchanged. + * Radius of curvature is NOT modified. + * + * @param {Object} parameters - Current Zernike surface parameters + * @returns {Object} Parameters with flipped Zernike coefficients + */ +export const flipZernikeAroundX = (parameters) => { + const result = { ...parameters }; + [3, 6, 8, 11, 13, 15, 18, 20, 22, 24, 27, 29, 31, 33, 35].forEach(n => { + const key = `Z${n}`; + if (result[key] !== undefined) result[key] = changeSign(result[key]); + }); + return result; +}; + +/** + * Flip Zernike surface around Y-axis (x → -x, θ → π-θ). + * Odd-m cosine (X) terms negated; even-m sine (Y) terms negated. + * Radius of curvature is NOT modified. + * + * @param {Object} parameters - Current Zernike surface parameters + * @returns {Object} Parameters with flipped Zernike coefficients + */ +export const flipZernikeAroundY = (parameters) => { + const result = { ...parameters }; + // odd-m X terms: m=1(Z2,Z7,Z14,Z23,Z34), m=3(Z10,Z19,Z30), m=5(Z26) + [2, 7, 10, 14, 19, 23, 26, 30, 34].forEach(n => { + const key = `Z${n}`; + if (result[key] !== undefined) result[key] = changeSign(result[key]); + }); + // even-m Y terms: m=2(Z6,Z13,Z22,Z33), m=4(Z18,Z29) + [6, 13, 18, 22, 29, 33].forEach(n => { + const key = `Z${n}`; + if (result[key] !== undefined) result[key] = changeSign(result[key]); + }); + return result; +}; + +/** + * Flip Zernike surface around Z-axis (θ → θ+π, x→-x and y→-y simultaneously). + * All odd-m terms (both X and Y) are negated; even-m and spherical terms unchanged. + * Radius of curvature is NOT modified. + * + * @param {Object} parameters - Current Zernike surface parameters + * @returns {Object} Parameters with flipped Zernike coefficients + */ +export const flipZernikeAroundZ = (parameters) => { + const result = { ...parameters }; + // odd-m: m=1(Z2,Z3,Z7,Z8,Z14,Z15,Z23,Z24,Z34,Z35), m=3(Z10,Z11,Z19,Z20,Z30,Z31), m=5(Z26,Z27) + [2, 3, 7, 8, 10, 11, 14, 15, 19, 20, 23, 24, 26, 27, 30, 31, 34, 35].forEach(n => { + const key = `Z${n}`; + if (result[key] !== undefined) result[key] = changeSign(result[key]); + }); + return result; +};