diff --git a/src/taskpane/components/RoundImage.tsx b/src/taskpane/components/RoundImage.tsx index 74b62323..2dff9958 100644 --- a/src/taskpane/components/RoundImage.tsx +++ b/src/taskpane/components/RoundImage.tsx @@ -1,24 +1,126 @@ import * as React from "react"; -import { - Button -} from "@fluentui/react-components"; -import { - CircleRegular -} from "@fluentui/react-icons"; +import { CircleRegular } from "@fluentui/react-icons"; import { useStatusContext } from "./App"; import { useCommonStyles } from "./commonStyles"; +import ActionButton from "./ActionButton"; import { getErrorMessage, isPictureShape, getFirstSelectedSlide, selectShapesById } from "../types/office-types"; +// Configuration constants +const MASK_COLOR = "red"; +const SUCCESS_MESSAGE = "Created mask shape. Please select both the image and the oval, then use the 'Shape Format > Merge Shapes > Intersect' command in PowerPoint."; + export const RoundImage: React.FC = () => { const styles = useCommonStyles(); - const { - statusMessage, setStatusMessage, - statusType, setStatusType, - isProcessing, setIsProcessing - } = useStatusContext(); + const { setStatusMessage, setStatusType } = useStatusContext(); - const convertToRoundImage = async () => { - setIsProcessing(true); + /** + * Validates that an image shape is selected + * @param shapes The collection of selected shapes + * @param context The PowerPoint request context + * @returns The selected image shape or null if validation fails + */ + const validateShapeSelection = async ( + shapes: PowerPoint.ShapeScopedCollection, + context: PowerPoint.RequestContext + ): Promise => { + // Check if any shape is selected + if (shapes.items.length === 0) { + setStatusMessage("No shapes are selected. Please select an image."); + setStatusType("warning"); + return null; + } + + // Get the first selected shape + const shape = shapes.items[0]; + + // Load essential properties + shape.load(["type"]); + await context.sync(); + + // Ensure the shape is a picture using our type-safe utility + if (!isPictureShape(shape)) { + setStatusMessage("Please select an image."); + setStatusType("warning"); + return null; + } + + return shape; + }; + + /** + * Creates an elliptical mask shape for the image + * @param slide The slide to add the mask to + * @param imageShape The image shape to mask + * @param context The PowerPoint request context + * @returns The created mask shape + */ + const createMaskShape = async ( + slide: PowerPoint.Slide, + imageShape: PowerPoint.Shape, + context: PowerPoint.RequestContext + ): Promise => { + // Load current dimensions to maintain aspect ratio + imageShape.load(["width", "height", "left", "top", "id"]); + await context.sync(); + + // Create elliptical mask with proper type + const maskShape = slide.shapes.addGeometricShape(PowerPoint.GeometricShapeType.ellipse); + + maskShape.load(["width", "height", "left", "top", "id"]); + await context.sync(); + + // Position the mask to match the image + maskShape.left = imageShape.left; + + imageShape.lineFormat.load(["weight"]); + await context.sync(); + + maskShape.top = imageShape.top; + maskShape.width = imageShape.width; + maskShape.height = imageShape.height; + + // Style the mask + maskShape.fill.setSolidColor(MASK_COLOR); + maskShape.lineFormat.visible = false; + + return maskShape; + }; + + /** + * Applies the mask to the image and selects both shapes + * @param slide The slide containing the shapes + * @param imageShape The image shape to mask + * @param maskShape The mask shape + * @param context The PowerPoint request context + */ + const applyMaskToImage = async ( + slide: PowerPoint.Slide, + imageShape: PowerPoint.Shape, + maskShape: PowerPoint.Shape, + context: PowerPoint.RequestContext + ): Promise => { + // Store original dimensions to maintain after selection + const width = imageShape.width; + const height = imageShape.height; + + // Select both shapes for the user to apply the intersection + selectShapesById(slide, [imageShape.id, maskShape.id]); + + // Ensure we maintain the same size + imageShape.width = width; + imageShape.height = height; + + await context.sync(); + + // Update status message with instructions + setStatusMessage(SUCCESS_MESSAGE); + setStatusType("warning"); + }; + + /** + * Main function to convert an image to a round image + */ + const convertToRoundImage = async (): Promise => { try { await PowerPoint.run(async (context) => { // Get the selected shapes @@ -26,89 +128,40 @@ export const RoundImage: React.FC = () => { shapes.load("items"); await context.sync(); - if (shapes.items.length === 0) { - setStatusMessage("No shapes are selected. Please select an image."); - setStatusType("warning"); + // Validate selection and get the image shape + const imageShape = await validateShapeSelection(shapes, context); + if (!imageShape) { return; } - // Get the first selected shape - const shape = shapes.items[0]; - - // Load essential properties - shape.load(["type"]); - await context.sync(); - - // Ensure the shape is a picture using our type-safe utility - if (!isPictureShape(shape)) { - setStatusMessage("Please select an image."); - setStatusType("warning"); - return; - } - - // Load current dimensions to maintain aspect ratio - shape.load(["width", "height", "left", "top", "id"]); - await context.sync(); - - // Store current dimensions - const width = shape.width; - const height = shape.height; - // Get the current slide using our type-safe utility const slide = getFirstSelectedSlide(context); - // Create elliptical mask with proper type - const maskShape = slide.shapes.addGeometricShape(PowerPoint.GeometricShapeType.ellipse); - - maskShape.load(["width", "height", "left", "top", "id"]); - await context.sync(); - - maskShape.left = shape.left; - - shape.lineFormat.load(["weight"]); - await context.sync(); - - maskShape.top = shape.top; // + shape.lineFormat.weight; - maskShape.width = shape.width; - maskShape.height = shape.height; - maskShape.fill.setSolidColor("red"); - maskShape.lineFormat.visible = false; - - setStatusMessage("Created mask shape. Please select both the image and the oval, then use the 'Shape Forma > Merge Shapes > Intersect' command in PowerPoint."); - setStatusType("warning"); - - // Use our type-safe utility for selecting shapes - selectShapesById(slide, [shape.id, maskShape.id]); - // Ensure we maintain the same size - shape.width = width; - shape.height = height; - - await context.sync(); + + // Create the mask shape + const maskShape = await createMaskShape(slide, imageShape, context); + + // Apply the mask and select both shapes + await applyMaskToImage(slide, imageShape, maskShape, context); }); } catch (error: unknown) { const errorMessage = getErrorMessage(error); setStatusMessage(`Error: ${errorMessage}`); setStatusType("error"); console.error("Round image error:", error); - } finally { - setIsProcessing(false); } }; return (
- + onClick={convertToRoundImage} + />
); }; -export default RoundImage; \ No newline at end of file +export default RoundImage;