Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 84 additions & 14 deletions web/src/insertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ type PreparedSvgResult = {
payload: string;
};

type SlideSize = {
width: number;
height: number;
};

/**
* Compiles Typst code to SVG and prepares it for insertion.
*/
Expand Down Expand Up @@ -98,12 +103,21 @@ export async function insertOrUpdateFormula() {
const selection = context.presentation.getSelectedShapes();
const selectedSlides = context.presentation.getSelectedSlides();
const allSlides = context.presentation.slides;
const pageSetup = context.presentation.pageSetup;

selection.load("items");
selectedSlides.load("items");
allSlides.load("items");
pageSetup.load(["slideWidth", "slideHeight"]);
await context.sync();

const slideSize: SlideSize = {
width: pageSetup.slideWidth,
height: pageSetup.slideHeight,
};

const fittedSize = fitSizeWithinSlide(prepared.size, slideSize);

const targetSlide: PowerPoint.Slide | undefined = selectedSlides.items[0] || allSlides.items[0];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!targetSlide || targetSlide.isNullObject) {
Expand All @@ -119,13 +133,14 @@ export async function insertOrUpdateFormula() {

const typstShape = await findTypstShape(selection.items, allSlides.items, context);
if (typstShape) {
position = calculateCenteredPosition(typstShape, prepared.size);
position = calculateCenteredPosition(typstShape, fittedSize);
position = clampPositionWithinSlide(position, fittedSize, slideSize);
rotation = typstShape.rotation;
typstShape.delete();
isReplacing = true;
await context.sync();
} else {
position = await calcShapeTopLeftToBeCentered(prepared.size, context);
position = calcShapeTopLeftToBeCentered(fittedSize, slideSize);
}

const existingShapeIds = new Set(targetSlide.shapes.items.map(shape => shape.id));
Expand All @@ -135,7 +150,7 @@ export async function insertOrUpdateFormula() {
fillColor: fillColor || null,
mathMode,
position,
size: prepared.size,
size: fittedSize,
rotation,
}, targetSlide.id, existingShapeIds);

Expand Down Expand Up @@ -236,6 +251,14 @@ export async function bulkUpdateFontSize() {
return;
}

const pageSetup = context.presentation.pageSetup;
pageSetup.load(["slideWidth", "slideHeight"]);
await context.sync();
const slideSize: SlideSize = {
width: pageSetup.slideWidth,
height: pageSetup.slideHeight,
};

let successCount = 0;

for (const shape of typstShapes) {
Expand All @@ -254,7 +277,13 @@ export async function bulkUpdateFontSize() {
continue;
}

const position = calculateCenteredPosition(shape, prepared.size);
const fittedSize = fitSizeWithinSlide(prepared.size, slideSize);
let position = calculateCenteredPosition(shape, fittedSize);
position = clampPositionWithinSlide(
position,
fittedSize,
slideSize,
);
const rotation = shape.rotation;

// Capture slide and existing shapes before deletion
Expand All @@ -274,7 +303,7 @@ export async function bulkUpdateFontSize() {
fillColor,
mathMode,
position,
size: prepared.size,
size: fittedSize,
rotation,
}, slideId, existingShapeIds);

Expand Down Expand Up @@ -309,19 +338,60 @@ function calculateCenteredPosition(
};
}

/**
* Scales a shape to fit the slide while preserving aspect ratio.
*
* The scale factor is computed as:
* s = min(slideWidth / shapeWidth, slideHeight / shapeHeight, 1)
*/
function fitSizeWithinSlide(
shapeSize: { width: number; height: number },
slideSize: SlideSize,
): { width: number; height: number } {
if (shapeSize.width <= 0 || shapeSize.height <= 0) {
return shapeSize;
}

const widthScale = slideSize.width / shapeSize.width;
const heightScale = slideSize.height / shapeSize.height;
const scale = Math.min(widthScale, heightScale, 1);

return {
width: shapeSize.width * scale,
height: shapeSize.height * scale,
};
}

/**
* Clamps a position so the full shape remains inside the slide.
*
* The placement is clamped to:
* - left: [0, slideWidth - shapeWidth]
* - top: [0, slideHeight - shapeHeight]
*/
function clampPositionWithinSlide(
position: { left: number; top: number },
shapeSize: { width: number; height: number },
slideSize: SlideSize,
): { left: number; top: number } {
const maxLeft = Math.max(0, slideSize.width - shapeSize.width);
const maxTop = Math.max(0, slideSize.height - shapeSize.height);

return {
left: Math.min(Math.max(0, position.left), maxLeft),
top: Math.min(Math.max(0, position.top), maxTop),
};
}

/**
* Calculates the top-left position for a shape to be centered on the slide.
*/
async function calcShapeTopLeftToBeCentered(
function calcShapeTopLeftToBeCentered(
shapeSize: { width: number; height: number },
context: PowerPoint.RequestContext,
slideSize: SlideSize,
) {
const pageSetup = context.presentation.pageSetup;
pageSetup.load(["slideWidth", "slideHeight"]);
await context.sync();

const centerX = (pageSetup.slideWidth - shapeSize.width) / 2;
const centerY = (pageSetup.slideHeight - shapeSize.height) / 2;
const centerX = (slideSize.width - shapeSize.width) / 2;
const centerY = (slideSize.height - shapeSize.height) / 2;

return { left: centerX, top: centerY };
return clampPositionWithinSlide({ left: centerX, top: centerY }, shapeSize, slideSize);
}
Loading