Refactor RoundImage component for improved maintainability and consistency with other components

This commit is contained in:
2025-03-22 18:11:23 +01:00
parent e0343dfd65
commit 1ed34ab888
+104 -51
View File
@@ -1,35 +1,33 @@
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 convertToRoundImage = async () => {
setIsProcessing(true);
try {
await PowerPoint.run(async (context) => {
// Get the selected shapes
const shapes = context.presentation.getSelectedShapes();
shapes.load("items");
await context.sync();
const { setStatusMessage, setStatusType } = useStatusContext();
/**
* 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<PowerPoint.Shape | null> => {
// Check if any shape is selected
if (shapes.items.length === 0) {
setStatusMessage("No shapes are selected. Please select an image.");
setStatusType("warning");
return;
return null;
}
// Get the first selected shape
@@ -43,69 +41,124 @@ export const RoundImage: React.FC = () => {
if (!isPictureShape(shape)) {
setStatusMessage("Please select an image.");
setStatusType("warning");
return;
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<PowerPoint.Shape> => {
// Load current dimensions to maintain aspect ratio
shape.load(["width", "height", "left", "top", "id"]);
imageShape.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;
// Position the mask to match the image
maskShape.left = imageShape.left;
shape.lineFormat.load(["weight"]);
imageShape.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.top = imageShape.top;
maskShape.width = imageShape.width;
maskShape.height = imageShape.height;
// Style the mask
maskShape.fill.setSolidColor(MASK_COLOR);
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");
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<void> => {
// 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]);
// 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;
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<void> => {
try {
await PowerPoint.run(async (context) => {
// Get the selected shapes
const shapes = context.presentation.getSelectedShapes();
shapes.load("items");
await context.sync();
// Validate selection and get the image shape
const imageShape = await validateShapeSelection(shapes, context);
if (!imageShape) {
return;
}
// Get the current slide using our type-safe utility
const slide = getFirstSelectedSlide(context);
// 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 (
<div className={styles.container}>
<div className={styles.buttonGroup}>
<Button
appearance="primary"
className={styles.actionButton}
onClick={convertToRoundImage}
<ActionButton
title="Round Image"
icon={<CircleRegular />}
disabled={isProcessing}
>
Round Image
</Button>
onClick={convertToRoundImage}
/>
</div>
</div>
);