diff --git a/src/components/panels/PropertiesPanel.js b/src/components/panels/PropertiesPanel.js index 0f203fe..3fd5893 100644 --- a/src/components/panels/PropertiesPanel.js +++ b/src/components/panels/PropertiesPanel.js @@ -35,6 +35,8 @@ export const PropertiesPanel = ({ handleFlipX, handleFlipY, handleFlipZ, + handleRotate90CCW, + handleRotate90CW, handleCopyCoefficients, c, t @@ -390,6 +392,8 @@ export const PropertiesPanel = ({ onFlipX: handleFlipX, onFlipY: handleFlipY, onFlipZ: handleFlipZ, + onRotate90CCW: handleRotate90CCW, + onRotate90CW: handleRotate90CW, onCopyCoefficients: handleCopyCoefficients, c, t diff --git a/src/components/ui/SurfaceActionButtons.js b/src/components/ui/SurfaceActionButtons.js index a995eb8..ed258e7 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, onFlipX, onFlipY, onFlipZ, onCopyCoefficients, c, t }) => { +export const SurfaceActionButtons = ({ surface, onInvert, onNormalizeUnZ, onConvertToUnZ, onConvertToPoly, onFlipX, onFlipY, onFlipZ, onRotate90CCW, onRotate90CW, onCopyCoefficients, c, t }) => { const buttonStyle = { padding: '8px 16px', backgroundColor: c.accent, @@ -94,6 +94,22 @@ export const SurfaceActionButtons = ({ surface, onInvert, onNormalizeUnZ, onConv title: 'Create a new surface with Zernike coefficients rotated 180° about the Z-axis (x → -x, y → -y)' }, t.properties.flipZ), + surface.type === 'Zernike' && h('button', { + onClick: onRotate90CCW, + 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 90° counter-clockwise' + }, t.properties.rotate90CCW), + + surface.type === 'Zernike' && h('button', { + onClick: onRotate90CW, + 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 90° clockwise' + }, t.properties.rotate90CW), + surface.type === 'Zernike' && h('button', { onClick: onCopyCoefficients, style: buttonStyle, diff --git a/src/constants/locales.js b/src/constants/locales.js index fc0dc24..5a20ba3 100644 --- a/src/constants/locales.js +++ b/src/constants/locales.js @@ -84,6 +84,8 @@ export const locales = { flipX: 'Flip around X', flipY: 'Flip around Y', flipZ: 'Flip around Z', + rotate90CCW: 'Rotate 90° CCW', + rotate90CW: 'Rotate 90° CW', copyCoefficients: 'Copy Coefficients', // Metrics @@ -497,6 +499,8 @@ export const locales = { flipX: 'Отразить по X', flipY: 'Отразить по Y', flipZ: 'Отразить по Z', + rotate90CCW: 'Повернуть 90° ПЧС', + rotate90CW: 'Повернуть 90° ЧС', copyCoefficients: 'Копировать коэффициенты', // Метрики diff --git a/src/renderer-modular.js b/src/renderer-modular.js index 6e6d5e0..6c90396 100644 --- a/src/renderer-modular.js +++ b/src/renderer-modular.js @@ -34,6 +34,8 @@ import { handleFlipZernikeX as flipZernikeXHandler, handleFlipZernikeY as flipZernikeYHandler, handleFlipZernikeZ as flipZernikeZHandler, + handleRotateZernike90CCW as rotateZernike90CCWHandler, + handleRotateZernike90CW as rotateZernike90CWHandler, handleCopyZernikeCoefficients as copyZernikeCoefficientsHandler } from './utils/surfaceOperationHandlers.js'; import { parseNumber } from './utils/numberParsing.js'; @@ -448,6 +450,14 @@ const OpticalSurfaceAnalyzer = () => { flipZernikeZHandler(selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface); }; + const handleRotate90CCW = () => { + rotateZernike90CCWHandler(selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface); + }; + + const handleRotate90CW = () => { + rotateZernike90CWHandler(selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface); + }; + const handleCopyCoefficients = () => { copyZernikeCoefficientsHandler(selectedSurface); }; @@ -1124,6 +1134,8 @@ const OpticalSurfaceAnalyzer = () => { handleFlipX, handleFlipY, handleFlipZ, + handleRotate90CCW, + handleRotate90CW, handleCopyCoefficients, c, t diff --git a/src/utils/surfaceOperationHandlers.js b/src/utils/surfaceOperationHandlers.js index ab8f76d..79fd301 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, flipZernikeAroundX, flipZernikeAroundY, flipZernikeAroundZ } from './surfaceTransformations.js'; +import { normalizeUnZ, convertPolyToUnZ, convertUnZToPoly, invertSurface, flipZernikeAroundX, flipZernikeAroundY, flipZernikeAroundZ, rotateZernikeBy90CCW, rotateZernikeBy90CW } from './surfaceTransformations.js'; import { parseNumber } from './numberParsing.js'; import { calculateSurfaceValues } from './calculations.js'; @@ -458,6 +458,34 @@ export const handleFlipZernikeZ = (selectedSurface, selectedFolder, folders, set } }; +/** + * Rotate Zernike surface 90° CCW and create a new surface. + */ +export const handleRotateZernike90CCW = (selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface) => { + if (!selectedSurface || selectedSurface.type !== 'Zernike' || !selectedFolder) return; + try { + const rotated = rotateZernikeBy90CCW(selectedSurface.parameters); + addFlippedZernikeSurface(selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface, rotated, 'rotated 90° CCW'); + } catch (error) { + alert(`Error rotating surface 90° CCW: ${error.message}`); + console.error('Rotate 90° CCW error:', error); + } +}; + +/** + * Rotate Zernike surface 90° CW and create a new surface. + */ +export const handleRotateZernike90CW = (selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface) => { + if (!selectedSurface || selectedSurface.type !== 'Zernike' || !selectedFolder) return; + try { + const rotated = rotateZernikeBy90CW(selectedSurface.parameters); + addFlippedZernikeSurface(selectedSurface, selectedFolder, folders, setFolders, setSelectedSurface, rotated, 'rotated 90° CW'); + } catch (error) { + alert(`Error rotating surface 90° CW: ${error.message}`); + console.error('Rotate 90° CW error:', error); + } +}; + /** * Copy all Zernike coefficients Z1-Z37 to clipboard as tab-separated values. */ diff --git a/src/utils/surfaceTransformations.js b/src/utils/surfaceTransformations.js index d8409fc..9ac3816 100644 --- a/src/utils/surfaceTransformations.js +++ b/src/utils/surfaceTransformations.js @@ -305,3 +305,95 @@ export const flipZernikeAroundZ = (parameters) => { }); return result; }; + +/** + * Rotate Zernike surface 90° CCW (θ → θ - π/2). + * Each azimuthal order m picks up a phase shift of mπ/2, mixing cos/sin pairs. + * + * m=0: unchanged + * m=1,5 (mπ/2 = π/2): new_cos = -old_sin, new_sin = old_cos + * m=2 (mπ/2 = π): both cos and sin negated + * m=3 (mπ/2 = 3π/2): new_cos = old_sin, new_sin = -old_cos + * m=4 (mπ/2 = 2π): unchanged + * + * @param {Object} parameters - Current Zernike surface parameters + * @returns {Object} Parameters rotated 90° CCW + */ +export const rotateZernikeBy90CCW = (parameters) => { + const result = { ...parameters }; + + // m=1 pairs: (Z2,Z3),(Z7,Z8),(Z14,Z15),(Z23,Z24),(Z34,Z35) → new_cos=-old_sin, new_sin=old_cos + [[2,3],[7,8],[14,15],[23,24],[34,35]].forEach(([c, s]) => { + const kc = `Z${c}`, ks = `Z${s}`; + const oldCos = result[kc], oldSin = result[ks]; + if (oldCos !== undefined) result[kc] = changeSign(oldSin); + if (oldSin !== undefined) result[ks] = oldCos; + }); + + // m=2: all negated + [5, 6, 12, 13, 21, 22, 32, 33].forEach(n => { + const key = `Z${n}`; + if (result[key] !== undefined) result[key] = changeSign(result[key]); + }); + + // m=3 pairs: (Z10,Z11),(Z19,Z20),(Z30,Z31) → new_cos=old_sin, new_sin=-old_cos + [[10,11],[19,20],[30,31]].forEach(([c, s]) => { + const kc = `Z${c}`, ks = `Z${s}`; + const oldCos = result[kc], oldSin = result[ks]; + if (oldCos !== undefined) result[kc] = oldSin; + if (oldSin !== undefined) result[ks] = changeSign(oldCos); + }); + + // m=5 pair: (Z26,Z27) → same rule as m=1 + const oldZ26 = result['Z26'], oldZ27 = result['Z27']; + if (oldZ26 !== undefined) result['Z26'] = changeSign(oldZ27); + if (oldZ27 !== undefined) result['Z27'] = oldZ26; + + return result; +}; + +/** + * Rotate Zernike surface 90° CW (θ → θ + π/2). + * Inverse of rotateZernikeBy90CCW. + * + * m=0: unchanged + * m=1,5 (mπ/2 = π/2): new_cos = old_sin, new_sin = -old_cos + * m=2 (mπ/2 = π): both cos and sin negated + * m=3 (mπ/2 = 3π/2): new_cos = -old_sin, new_sin = old_cos + * m=4 (mπ/2 = 2π): unchanged + * + * @param {Object} parameters - Current Zernike surface parameters + * @returns {Object} Parameters rotated 90° CW + */ +export const rotateZernikeBy90CW = (parameters) => { + const result = { ...parameters }; + + // m=1 pairs: new_cos=old_sin, new_sin=-old_cos + [[2,3],[7,8],[14,15],[23,24],[34,35]].forEach(([c, s]) => { + const kc = `Z${c}`, ks = `Z${s}`; + const oldCos = result[kc], oldSin = result[ks]; + if (oldCos !== undefined) result[kc] = oldSin; + if (oldSin !== undefined) result[ks] = changeSign(oldCos); + }); + + // m=2: all negated + [5, 6, 12, 13, 21, 22, 32, 33].forEach(n => { + const key = `Z${n}`; + if (result[key] !== undefined) result[key] = changeSign(result[key]); + }); + + // m=3 pairs: new_cos=-old_sin, new_sin=old_cos + [[10,11],[19,20],[30,31]].forEach(([c, s]) => { + const kc = `Z${c}`, ks = `Z${s}`; + const oldCos = result[kc], oldSin = result[ks]; + if (oldCos !== undefined) result[kc] = changeSign(oldSin); + if (oldSin !== undefined) result[ks] = oldCos; + }); + + // m=5 pair: same rule as m=1 + const oldZ26 = result['Z26'], oldZ27 = result['Z27']; + if (oldZ26 !== undefined) result['Z26'] = oldZ27; + if (oldZ27 !== undefined) result['Z27'] = changeSign(oldZ26); + + return result; +};