import * as React from "react"; import { Button, makeStyles, Label, Slider, SpinButton, Divider, ToggleButton, tokens, Input } from "@fluentui/react-components"; import { GridRegular, DismissRegular, AddRegular, LineHorizontal3Regular, SplitVerticalRegular, GridDotsRegular, ArrowResetRegular } from "@fluentui/react-icons"; import { useStatusContext } from "./App"; import { useCommonStyles } from "./commonStyles"; const useStyles = makeStyles({ buttonGrid: { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: "8px", width: "100%", marginBottom: "8px" }, gridButton: { width: "100%", minWidth: 0, padding: "8px 4px", "& span": { justifyContent: "center" } }, controlRow: { display: "flex", alignItems: "center", gap: "8px", marginBottom: "8px" }, controlLabel: { minWidth: "80px", marginBottom: "0" }, controlInput: { flex: 1, "& input[type=number]::-webkit-inner-spin-button, & input[type=number]::-webkit-outer-spin-button": { "-webkit-appearance": "none", margin: 0 }, "& input[type=number]": { "-moz-appearance": "textfield" } }, guidesContainer: { marginTop: "16px" }, guideButtonGrid: { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: "8px", width: "100%", marginTop: "8px" }, colorButton: { width: "24px", height: "24px", minWidth: "24px", padding: "0", border: "1px solid " + tokens.colorNeutralStroke1, marginLeft: "8px" }, colorButtonSelected: { border: "2px solid " + tokens.colorBrandForeground1 } }); type GridColor = "blue" | "red" | "yellow" | "green"; const GRID_PREFIX = "edison_grid_"; const GUIDE_PREFIX = "edison_guide_"; const colorValues = { blue: "#4472C4", red: "#C00000", yellow: "#FFC000", green: "#70AD47" }; export const GridGuidelineManager: React.FC = () => { const styles = useStyles(); const commonStyles = useCommonStyles(); const { statusMessage, setStatusMessage, statusType, setStatusType, isProcessing, setIsProcessing } = useStatusContext(); // Grid state const [gridSpacing, setGridSpacing] = React.useState(50); const [gridColor, setGridColor] = React.useState("blue"); const [gridOpacity, setGridOpacity] = React.useState(30); // Guidelines state const [guidePosition, setGuidePosition] = React.useState(240); const [guideDirection, setGuideDirection] = React.useState<"horizontal" | "vertical">("horizontal"); const [guideColor, setGuideColor] = React.useState("red"); // Helper function to create a line shape const createLineShape = ( slide: PowerPoint.Slide, startX: number, startY: number, endX: number, endY: number ) => { // Create a line using alternative approach with a rectangle shape const line = slide.shapes.addGeometricShape("Rectangle"); if (startX === endX) { // Vertical line line.left = startX; line.top = startY; line.height = endY - startY; line.width = 1; // Make it 1pt wide } else { // Horizontal line line.left = startX; line.top = startY; line.width = endX - startX; line.height = 1; // Make it 1pt tall } return line; }; const createGrid = async () => { setIsProcessing(true); try { await PowerPoint.run(async (context) => { // Remove any existing grid first await removeGrid(false); // Get the current slide using the selected slides collection const selectedSlides = context.presentation.getSelectedSlides(); selectedSlides.load("items"); await context.sync(); if (selectedSlides.items.length === 0) { throw new Error("No slide selected"); } const currentSlide = selectedSlides.items[0]; // Standard dimensions for PowerPoint slides const slideWidth = 960; // Width in points const slideHeight = 540; // Height in points // Calculate number of lines needed const numHorizontalLines = Math.floor(slideHeight / gridSpacing); const numVerticalLines = Math.floor(slideWidth / gridSpacing); // Create horizontal grid lines for (let i = 1; i <= numHorizontalLines; i++) { const yPosition = i * gridSpacing; // Skip if we're at the edge of the slide if (yPosition >= slideHeight) continue; // Create horizontal line const line = createLineShape(currentSlide, 0, yPosition, slideWidth, yPosition); line.name = `${GRID_PREFIX}h_${i}`; // Set line properties line.fill.setSolidColor(colorValues[gridColor]); line.lineFormat.weight = 0.75; // Thin line line.lineFormat.visible = false; // No outline, just fill // Set transparency (opacity is inverse of transparency) line.fill.transparency = (100 - gridOpacity) / 100; } // Create vertical grid lines for (let i = 1; i <= numVerticalLines; i++) { const xPosition = i * gridSpacing; // Skip if we're at the edge of the slide if (xPosition >= slideWidth) continue; // Create vertical line const line = createLineShape(currentSlide, xPosition, 0, xPosition, slideHeight); line.name = `${GRID_PREFIX}v_${i}`; // Set line properties line.fill.setSolidColor(colorValues[gridColor]); line.lineFormat.weight = 0.75; // Thin line line.lineFormat.visible = false; // No outline, just fill // Set transparency (opacity is inverse of transparency) line.fill.transparency = (100 - gridOpacity) / 100; } setStatusMessage("Grid created on current slide"); setStatusType("success"); }); } catch (error) { setStatusMessage(`Error: ${error.message}`); setStatusType("error"); console.error("Create grid error:", error); } finally { setIsProcessing(false); } }; const removeGrid = async (showStatus = true) => { if (showStatus) { setIsProcessing(true); } try { await PowerPoint.run(async (context) => { // Get the current slide using the selected slides collection const selectedSlides = context.presentation.getSelectedSlides(); selectedSlides.load("items"); await context.sync(); if (selectedSlides.items.length === 0) { throw new Error("No slide selected"); } const currentSlide = selectedSlides.items[0]; // Load all shapes on this slide const shapes = currentSlide.shapes; shapes.load("items"); await context.sync(); // Load all shape names at once const allShapes = shapes.items; for (let i = 0; i < allShapes.length; i++) { allShapes[i].load("name"); } await context.sync(); // Find shapes that are part of our grid and mark them for deletion let removedCount = 0; for (let i = 0; i < allShapes.length; i++) { const shape = allShapes[i]; // Check if this shape is part of our grid if (shape.name && shape.name.startsWith(GRID_PREFIX)) { shape.delete(); removedCount++; } } // Single sync after all deletions await context.sync(); if (showStatus) { if (removedCount > 0) { setStatusMessage(`Removed grid from slide`); } else { setStatusMessage("No grid found to remove"); } setStatusType("success"); } }); } catch (error) { if (showStatus) { setStatusMessage(`Error: ${error.message}`); setStatusType("error"); } console.error("Remove grid error:", error); } finally { if (showStatus) { setIsProcessing(false); } } }; const addGuideline = async () => { setIsProcessing(true); try { await PowerPoint.run(async (context) => { // Get the current slide using the selected slides collection const selectedSlides = context.presentation.getSelectedSlides(); selectedSlides.load("items"); await context.sync(); if (selectedSlides.items.length === 0) { throw new Error("No slide selected"); } const currentSlide = selectedSlides.items[0]; // Standard dimensions for PowerPoint slides const slideWidth = 960; // Width in points const slideHeight = 540; // Height in points // Create a unique identifier for this guide const timestamp = new Date().getTime(); const guideId = `${GUIDE_PREFIX}${guideDirection}_${timestamp}`; // Add the guide line let line; if (guideDirection === "horizontal") { // Create horizontal guideline line = createLineShape(currentSlide, 0, guidePosition, slideWidth, guidePosition); } else { // Create vertical guideline line = createLineShape(currentSlide, guidePosition, 0, guidePosition, slideHeight); } line.name = guideId; // Set line properties line.fill.setSolidColor(colorValues[guideColor]); line.lineFormat.visible = false; // No outline, just fill // We want solid lines for guidelines line.fill.transparency = 0; setStatusMessage(`Added ${guideDirection} guideline`); setStatusType("success"); }); } catch (error) { setStatusMessage(`Error: ${error.message}`); setStatusType("error"); console.error("Add guideline error:", error); } finally { setIsProcessing(false); } }; const removeGuidelines = async () => { setIsProcessing(true); try { await PowerPoint.run(async (context) => { // Get the current slide using the selected slides collection const selectedSlides = context.presentation.getSelectedSlides(); selectedSlides.load("items"); await context.sync(); if (selectedSlides.items.length === 0) { throw new Error("No slide selected"); } const currentSlide = selectedSlides.items[0]; // Load all shapes on this slide const shapes = currentSlide.shapes; shapes.load("items"); await context.sync(); // Find shapes that are guidelines let removedCount = 0; for (let i = 0; i < shapes.items.length; i++) { const shape = shapes.items[i]; shape.load("name"); await context.sync(); // Check if this shape is a guideline if (shape.name && shape.name.startsWith(GUIDE_PREFIX)) { shape.delete(); removedCount++; } } await context.sync(); if (removedCount > 0) { setStatusMessage(`Removed ${removedCount} guidelines`); } else { setStatusMessage("No guidelines found to remove"); } setStatusType("success"); }); } catch (error) { setStatusMessage(`Error: ${error.message}`); setStatusType("error"); console.error("Remove guidelines error:", error); } finally { setIsProcessing(false); } }; // Color picker button component const ColorButton = ({ color, selectedColor, onClick }: { color: GridColor, selectedColor: GridColor, onClick: (color: GridColor) => void }) => { return ( {/* Guidelines Section */}
{ const newValue = e.target.value; const parsed = parseInt(newValue); if (newValue === '') { setGuidePosition(0); } else if (!isNaN(parsed)) { setGuidePosition(parsed); } }} onClick={(e) => (e.target as HTMLInputElement).select()} style={{ width: "100%", padding: "5px 8px", border: "1px solid #d1d1d1", borderRadius: "4px", fontSize: "14px" }} />
setGuideDirection("horizontal")} icon={} style={{ minWidth: "40px", padding: "2px 8px" }} > Horizontal setGuideDirection("vertical")} icon={} style={{ minWidth: "40px", padding: "2px 8px" }} > Vertical
); }; export default GridGuidelineManager;