Initial commit of grid line function

This commit is contained in:
2025-03-13 00:30:31 +01:00
parent f0773d93d8
commit 044bd9ebec
4 changed files with 609 additions and 0 deletions
+5
View File
@@ -25,6 +25,11 @@ A productivity toolkit that enhances PowerPoint with specialized formatting and
- **Horizontal Alignment**: Align selected objects to the left, center, or right of the slide.
- **Vertical Alignment**: Align selected objects to the top, middle, or bottom of the slide.
### Grid & Guidelines Tools
- **Custom Grid**: Create a customizable grid with adjustable spacing, opacity, and color.
- **Guidelines**: Add horizontal or vertical guidelines at specific positions with customizable colors.
## Installation
### For End Users
+34
View File
@@ -134,6 +134,40 @@ These tools align selected objects horizontally or vertically on the slide.
- **Middle** - Centers all shapes vertically
- **Bottom** - Aligns all shapes to the bottom edge
## Grid & Guidelines Tools
These tools help you align and position elements precisely on slides using grids and guidelines.
### Custom Grid
Creates a customizable grid overlay on the current slide to help with precise positioning and alignment.
**How to use:**
1. Navigate to the slide where you want to add a grid
2. In the Grid & Guidelines section, configure the grid options:
- **Spacing** - Set the distance between grid lines (in points)
- **Opacity** - Adjust how visible the grid appears
- **Color** - Choose from blue, red, yellow, or green
3. Click "Create Grid" to add the grid to the current slide
4. Click "Remove Grid" to clear the grid from the slide
**Note:** The grid appears only on the current slide and is for visual reference only - it won't appear in presentation mode or exports.
### Guidelines
Add precise horizontal or vertical guidelines at specific positions on your slide.
**How to use:**
1. Navigate to the slide where you want to add guidelines
2. In the Grid & Guidelines section:
- **Position** - Set the position of the guideline (in points from top/left)
- **Type** - Choose horizontal or vertical
- **Color** - Select a color for the guideline
3. Click "Add Guideline" to add the specified guideline
4. Click "Remove All" to clear all guidelines from the slide
**Tip:** Use different colors for different types of guidelines (e.g., blue for margins, red for key alignment points).
## Tips and Best Practices
### Selection Order Matters
+9
View File
@@ -9,6 +9,7 @@ import ConfidentialButtons from "./ConfidentialButtons";
import DraftButtons from "./DraftButtons";
import ProgressBarButtons from "./ProgressBarButtons";
import AlignmentButtons from "./AlignmentButtons";
import GridGuidelineManager from "./GridGuidelineManager";
import { makeStyles, Text, Subtitle1, tokens, Theme, Spinner } from "@fluentui/react-components";
import { ShapeUnionRegular, SquareRegular, InfoRegular } from "@fluentui/react-icons";
@@ -46,6 +47,7 @@ const useStyles = makeStyles({
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
backgroundColor: tokens.colorNeutralBackground1,
overflow: "auto",
maxHeight: "100vh",
},
statusContainer: {
marginTop: "4px",
@@ -194,6 +196,13 @@ const App: React.FC<AppProps> = () => {
<AlignmentButtons />
</div>
<div className={styles.section}>
<Subtitle1 block className={styles.sectionTitle}>
Grid & Guidelines
</Subtitle1>
<GridGuidelineManager />
</div>
{/* Status message area at the bottom */}
{isProcessing && (
<div className={styles.statusContainer}>
@@ -0,0 +1,561 @@
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
},
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<GridColor>("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<GridColor>("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();
// Find shapes that are part of our grid
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 part of our grid
if (shape.name && shape.name.startsWith(GRID_PREFIX)) {
shape.delete();
removedCount++;
}
}
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 (
<Button
className={`${styles.colorButton} ${color === selectedColor ? styles.colorButtonSelected : ''}`}
style={{ backgroundColor: colorValues[color] }}
onClick={() => onClick(color)}
title={color.charAt(0).toUpperCase() + color.slice(1)}
/>
);
};
return (
<div className={commonStyles.container} style={{ maxHeight: "400px", overflowY: "auto" }}>
{/* Grid Controls */}
<div className={styles.buttonGrid}>
<Button
appearance="primary"
className={styles.gridButton}
onClick={createGrid}
icon={<GridDotsRegular />}
disabled={isProcessing}
title="Create Grid"
>
Create Grid
</Button>
<Button
appearance="primary"
className={styles.gridButton}
onClick={() => removeGrid(true)}
icon={<DismissRegular />}
disabled={isProcessing}
title="Remove Grid"
>
Remove Grid
</Button>
</div>
<div className={styles.controlRow}>
<Label className={styles.controlLabel}>Spacing</Label>
<Input
className={styles.controlInput}
value={gridSpacing.toString()}
type="number"
min={10}
max={200}
onChange={(_event, data) => {
const value = parseInt(data.value);
if (!isNaN(value) && value >= 10 && value <= 200) {
setGridSpacing(value);
}
}}
/>
</div>
<div className={styles.controlRow}>
<Label className={styles.controlLabel}>Opacity</Label>
<div style={{ display: "flex", flex: 1, gap: "8px", alignItems: "center" }}>
<Input
className={styles.controlInput}
style={{ width: "70px" }}
value={gridOpacity.toString()}
type="number"
min={10}
max={100}
onChange={(_event, data) => {
const value = parseInt(data.value);
if (!isNaN(value) && value >= 10 && value <= 100) {
setGridOpacity(value);
}
}}
/>
<Slider
style={{ flex: 1 }}
value={gridOpacity}
min={10}
max={100}
step={10}
onChange={(_event, data) => setGridOpacity(data.value)}
/>
</div>
</div>
<div className={styles.controlRow}>
<Label className={styles.controlLabel}>Color</Label>
<div style={{ display: "flex" }}>
<ColorButton color="blue" selectedColor={gridColor} onClick={setGridColor} />
<ColorButton color="red" selectedColor={gridColor} onClick={setGridColor} />
<ColorButton color="yellow" selectedColor={gridColor} onClick={setGridColor} />
<ColorButton color="green" selectedColor={gridColor} onClick={setGridColor} />
</div>
</div>
{/* Guidelines Section */}
<div className={styles.guidesContainer}>
<Divider />
<Label style={{ marginTop: "12px", marginBottom: "8px" }}>Guidelines</Label>
<div className={styles.controlRow}>
<Label className={styles.controlLabel}>Position</Label>
<Input
className={styles.controlInput}
value={guidePosition.toString()}
type="number"
min={0}
max={guideDirection === "horizontal" ? 540 : 960}
onChange={(_event, data) => {
const value = parseInt(data.value);
const maxVal = guideDirection === "horizontal" ? 540 : 960;
if (!isNaN(value) && value >= 0 && value <= maxVal) {
setGuidePosition(value);
}
}}
/>
</div>
<div className={styles.controlRow}>
<Label className={styles.controlLabel}>Type</Label>
<div style={{ display: "flex", gap: "8px" }}>
<ToggleButton
size="small"
appearance="outline"
checked={guideDirection === "horizontal"}
onClick={() => setGuideDirection("horizontal")}
icon={<LineHorizontal3Regular />}
style={{ minWidth: "40px", padding: "2px 8px" }}
>
Horizontal
</ToggleButton>
<ToggleButton
size="small"
appearance="outline"
checked={guideDirection === "vertical"}
onClick={() => setGuideDirection("vertical")}
icon={<SplitVerticalRegular />}
style={{ minWidth: "40px", padding: "2px 8px" }}
>
Vertical
</ToggleButton>
</div>
</div>
<div className={styles.controlRow}>
<Label className={styles.controlLabel}>Color</Label>
<div style={{ display: "flex" }}>
<ColorButton color="blue" selectedColor={guideColor} onClick={setGuideColor} />
<ColorButton color="red" selectedColor={guideColor} onClick={setGuideColor} />
<ColorButton color="yellow" selectedColor={guideColor} onClick={setGuideColor} />
<ColorButton color="green" selectedColor={guideColor} onClick={setGuideColor} />
</div>
</div>
<div className={styles.guideButtonGrid}>
<Button
appearance="primary"
className={styles.gridButton}
onClick={addGuideline}
icon={<AddRegular />}
disabled={isProcessing}
title="Add Guideline"
>
Add Guideline
</Button>
<Button
appearance="primary"
className={styles.gridButton}
onClick={removeGuidelines}
icon={<ArrowResetRegular />}
disabled={isProcessing}
title="Remove All Guidelines"
>
Remove All
</Button>
</div>
</div>
</div>
);
};
export default GridGuidelineManager;