Skip to content

Commit 3ad464f

Browse files
committed
Add canvas resizing
1 parent 63a0ffc commit 3ad464f

7 files changed

Lines changed: 278 additions & 6 deletions

File tree

contracts/api-registry.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2318,6 +2318,41 @@
23182318
"recommended": true
23192319
}
23202320
},
2321+
{
2322+
"sdkName": "resizeQuiltCanvas",
2323+
"method": "POST",
2324+
"path": "/quilts/{quiltSlug}/resize",
2325+
"tag": "Quilts",
2326+
"summary": "Resize a quilt canvas and record the resize in history",
2327+
"requestBody": true,
2328+
"parameters": [
2329+
{
2330+
"name": "quiltSlug",
2331+
"in": "path",
2332+
"required": true,
2333+
"schema": {
2334+
"type": "string"
2335+
}
2336+
}
2337+
],
2338+
"requestExample": {
2339+
"width": 128,
2340+
"height": 128,
2341+
"offsetX": 8,
2342+
"offsetY": 4
2343+
},
2344+
"auth": {
2345+
"required": true,
2346+
"optional": false,
2347+
"kind": "user",
2348+
"label": "Requires moderator login"
2349+
},
2350+
"visibility": "public",
2351+
"rateLimit": {
2352+
"documented": true,
2353+
"headers": true
2354+
}
2355+
},
23212356
{
23222357
"sdkName": "voteQuiltSubmission",
23232358
"method": "POST",

features/quilts/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ export {
44
getQuiltDetail,
55
listQuilts,
66
quiltCreateSchema,
7+
quiltResizeSchema,
78
quiltSlugParamsSchema,
89
quiltSubmissionParamsSchema,
910
quiltSubmissionSchema,
1011
quiltVoteSchema,
1112
removeQuiltSubmission,
13+
resizeQuilt,
1214
submitQuiltPixels,
1315
updateQuiltSubmission,
1416
voteQuiltSubmission,

features/quilts/router.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import {
1414
getQuiltDetail,
1515
listQuilts,
1616
quiltCreateSchema,
17+
quiltResizeSchema,
1718
quiltSlugParamsSchema,
1819
quiltSubmissionParamsSchema,
1920
quiltSubmissionSchema,
2021
quiltVoteSchema,
2122
removeQuiltSubmission,
23+
resizeQuilt,
2224
submitQuiltPixels,
2325
updateQuiltSubmission,
2426
voteQuiltSubmission,
@@ -86,6 +88,25 @@ export function createQuiltsRouter() {
8688
}),
8789
);
8890

91+
router.post(
92+
"/:quiltSlug/resize",
93+
rateLimit(),
94+
authUser,
95+
getUser,
96+
asyncHandler(async (req, res) => {
97+
const { quiltSlug } = parseParams(req, quiltSlugParamsSchema);
98+
const input = parseBody(req, quiltResizeSchema);
99+
res.json({
100+
data: await resizeQuilt({
101+
slug: quiltSlug,
102+
input,
103+
actor: requireRequestUser(res),
104+
tenantId: res.locals.tenantId,
105+
}),
106+
});
107+
}),
108+
);
109+
89110
router.post(
90111
"/submissions/:submissionId/accept",
91112
rateLimit(),

features/quilts/service.ts

Lines changed: 190 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { QuiltSubmissionStatus } from "@prisma/client";
1+
import { QuiltSubmissionKind, QuiltSubmissionStatus } from "@prisma/client";
22
import { z } from "zod";
33

44
import { appConfig } from "../../config/app.js";
@@ -41,6 +41,13 @@ export const quiltVoteSchema = z.object({
4141
value: z.coerce.number().int().refine((value) => value === 1 || value === -1),
4242
});
4343

44+
export const quiltResizeSchema = z.object({
45+
width: z.coerce.number().int().min(8).max(512),
46+
height: z.coerce.number().int().min(8).max(512),
47+
offsetX: z.coerce.number().int().min(-512).max(512),
48+
offsetY: z.coerce.number().int().min(-512).max(512),
49+
});
50+
4451
export const quiltSlugParamsSchema = z.object({
4552
quiltSlug: z.string().trim().min(1),
4653
});
@@ -61,7 +68,14 @@ type QuiltPixel = z.infer<typeof quiltPixelSchema>;
6168

6269
type QuiltSubmissionWithRelations = {
6370
id: number;
71+
kind: QuiltSubmissionKind;
6472
pixels: unknown;
73+
canvasWidth: number | null;
74+
canvasHeight: number | null;
75+
resizeFromWidth: number | null;
76+
resizeFromHeight: number | null;
77+
resizeOffsetX: number | null;
78+
resizeOffsetY: number | null;
6579
status: QuiltSubmissionStatus;
6680
resolvesAt: Date;
6781
resolvedAt: Date | null;
@@ -118,7 +132,14 @@ function serializeSubmission(
118132
) {
119133
return {
120134
id: submission.id,
135+
kind: submission.kind,
121136
pixels: parsePixels(submission.pixels),
137+
canvasWidth: submission.canvasWidth,
138+
canvasHeight: submission.canvasHeight,
139+
resizeFromWidth: submission.resizeFromWidth,
140+
resizeFromHeight: submission.resizeFromHeight,
141+
resizeOffsetX: submission.resizeOffsetX,
142+
resizeOffsetY: submission.resizeOffsetY,
122143
status: submission.status,
123144
score: scoreSubmission(submission),
124145
viewerVote:
@@ -134,16 +155,58 @@ function serializeSubmission(
134155
function composeCanvas(
135156
width: number,
136157
height: number,
137-
submissions: Array<{ id: number; pixels: unknown }>,
158+
submissions: Array<{
159+
id: number;
160+
kind?: QuiltSubmissionKind;
161+
pixels: unknown;
162+
canvasWidth?: number | null;
163+
canvasHeight?: number | null;
164+
resizeFromWidth?: number | null;
165+
resizeFromHeight?: number | null;
166+
resizeOffsetX?: number | null;
167+
resizeOffsetY?: number | null;
168+
}>,
138169
) {
139-
const stacks = Array.from({ length: width * height }, () => [] as string[]);
170+
let activeWidth = width;
171+
let activeHeight = height;
172+
let stacks = Array.from({ length: activeWidth * activeHeight }, () => [] as string[]);
140173

141174
for (const submission of submissions) {
175+
if (submission.kind === QuiltSubmissionKind.RESIZE) {
176+
const nextWidth = submission.canvasWidth ?? activeWidth;
177+
const nextHeight = submission.canvasHeight ?? activeHeight;
178+
const offsetX = submission.resizeOffsetX ?? 0;
179+
const offsetY = submission.resizeOffsetY ?? 0;
180+
const nextStacks = Array.from(
181+
{ length: nextWidth * nextHeight },
182+
() => [] as string[],
183+
);
184+
for (let y = 0; y < activeHeight; y++) {
185+
for (let x = 0; x < activeWidth; x++) {
186+
const nx = x + offsetX;
187+
const ny = y + offsetY;
188+
if (nx < 0 || ny < 0 || nx >= nextWidth || ny >= nextHeight) {
189+
continue;
190+
}
191+
nextStacks[ny * nextWidth + nx] = [...stacks[y * activeWidth + x]];
192+
}
193+
}
194+
activeWidth = nextWidth;
195+
activeHeight = nextHeight;
196+
stacks = nextStacks;
197+
continue;
198+
}
199+
142200
for (const pixel of parsePixels(submission.pixels)) {
143-
if (pixel.x < 0 || pixel.x >= width || pixel.y < 0 || pixel.y >= height) {
201+
if (
202+
pixel.x < 0 ||
203+
pixel.x >= activeWidth ||
204+
pixel.y < 0 ||
205+
pixel.y >= activeHeight
206+
) {
144207
continue;
145208
}
146-
const index = pixel.y * width + pixel.x;
209+
const index = pixel.y * activeWidth + pixel.x;
147210
if (pixel.color === null) {
148211
stacks[index].pop();
149212
} else {
@@ -152,7 +215,39 @@ function composeCanvas(
152215
}
153216
}
154217

155-
return stacks.map((stack) => stack.at(-1) ?? null);
218+
const canvas = stacks.map((stack) => stack.at(-1) ?? null);
219+
if (activeWidth === width && activeHeight === height) {
220+
return canvas;
221+
}
222+
const normalized = Array.from({ length: width * height }, () => null as string | null);
223+
for (let y = 0; y < Math.min(activeHeight, height); y++) {
224+
for (let x = 0; x < Math.min(activeWidth, width); x++) {
225+
normalized[y * width + x] = canvas[y * activeWidth + x];
226+
}
227+
}
228+
return normalized;
229+
}
230+
231+
function transformPixelsForResize(
232+
pixels: QuiltPixel[],
233+
width: number,
234+
height: number,
235+
offsetX: number,
236+
offsetY: number,
237+
) {
238+
return pixels
239+
.map((pixel) => ({
240+
x: pixel.x + offsetX,
241+
y: pixel.y + offsetY,
242+
color: pixel.color,
243+
}))
244+
.filter(
245+
(pixel) =>
246+
pixel.x >= 0 &&
247+
pixel.y >= 0 &&
248+
pixel.x < width &&
249+
pixel.y < height,
250+
);
156251
}
157252

158253
async function resolveDueSubmissions(quiltId: number) {
@@ -357,6 +452,8 @@ export async function submitQuiltPixels({
357452
quiltId: quilt.id,
358453
authorId: actor.id,
359454
pixels: normalizedPixels,
455+
canvasWidth: quilt.width,
456+
canvasHeight: quilt.height,
360457
resolvesAt: new Date(now.getTime() + REVIEW_WINDOW_MS),
361458
},
362459
});
@@ -425,6 +522,8 @@ export async function updateQuiltSubmission({
425522
where: { id: submissionId },
426523
data: {
427524
pixels: Array.from(unique.values()),
525+
canvasWidth: submission.quilt.width,
526+
canvasHeight: submission.quilt.height,
428527
status: QuiltSubmissionStatus.PENDING,
429528
resolvesAt: new Date(now.getTime() + REVIEW_WINDOW_MS),
430529
resolvedAt: null,
@@ -438,6 +537,87 @@ export async function updateQuiltSubmission({
438537
return getQuiltDetail({ slug: submission.quilt.slug, actor, tenantId });
439538
}
440539

540+
export async function resizeQuilt({
541+
slug,
542+
input,
543+
actor,
544+
tenantId,
545+
}: {
546+
slug: string;
547+
input: z.infer<typeof quiltResizeSchema>;
548+
actor: QuiltActor;
549+
tenantId?: string | null;
550+
}) {
551+
assertAdmin(actor);
552+
const quilt = await getQuiltOrThrow(slug, tenantId);
553+
if (
554+
input.width === quilt.width &&
555+
input.height === quilt.height &&
556+
input.offsetX === 0 &&
557+
input.offsetY === 0
558+
) {
559+
throw new BadRequestError("Canvas size did not change.");
560+
}
561+
562+
const submissions = await db.quiltSubmission.findMany({
563+
where: {
564+
quiltId: quilt.id,
565+
status: {
566+
in: [
567+
QuiltSubmissionStatus.PENDING,
568+
QuiltSubmissionStatus.USER_DELETED,
569+
],
570+
},
571+
},
572+
select: { id: true, pixels: true },
573+
});
574+
575+
await db.$transaction([
576+
db.quilt.update({
577+
where: { id: quilt.id },
578+
data: {
579+
width: input.width,
580+
height: input.height,
581+
},
582+
}),
583+
db.quiltSubmission.create({
584+
data: {
585+
quiltId: quilt.id,
586+
authorId: actor.id,
587+
kind: QuiltSubmissionKind.RESIZE,
588+
pixels: [],
589+
canvasWidth: input.width,
590+
canvasHeight: input.height,
591+
resizeFromWidth: quilt.width,
592+
resizeFromHeight: quilt.height,
593+
resizeOffsetX: input.offsetX,
594+
resizeOffsetY: input.offsetY,
595+
status: QuiltSubmissionStatus.ACCEPTED,
596+
resolvesAt: new Date(),
597+
resolvedAt: new Date(),
598+
},
599+
}),
600+
...submissions.map((submission) =>
601+
db.quiltSubmission.update({
602+
where: { id: submission.id },
603+
data: {
604+
pixels: transformPixelsForResize(
605+
parsePixels(submission.pixels),
606+
input.width,
607+
input.height,
608+
input.offsetX,
609+
input.offsetY,
610+
),
611+
canvasWidth: input.width,
612+
canvasHeight: input.height,
613+
},
614+
}),
615+
),
616+
]);
617+
618+
return getQuiltDetail({ slug, actor, tenantId });
619+
}
620+
441621
export async function voteQuiltSubmission({
442622
submissionId,
443623
input,
@@ -538,13 +718,17 @@ export async function removeQuiltSubmission({
538718
select: {
539719
id: true,
540720
authorId: true,
721+
kind: true,
541722
status: true,
542723
quilt: { select: { slug: true } },
543724
},
544725
});
545726
if (!submission) {
546727
throw new NotFoundError("Quilt submission not found.");
547728
}
729+
if (submission.kind === QuiltSubmissionKind.RESIZE) {
730+
throw new BadRequestError("Canvas resize history cannot be removed.");
731+
}
548732
const isOwnPending =
549733
submission.authorId === actor.id &&
550734
submission.status === QuiltSubmissionStatus.PENDING;

generated/sdk.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export function createJamcoreClient(config: JamcoreClientConfig = {}) {
111111
createQuilt: (body: unknown) => request("POST", "/quilts", { body }),
112112
getQuilt: (quiltSlug: string) => request("GET", `/quilts/${quiltSlug}`, { }),
113113
submitQuiltPixels: (quiltSlug: string, body: unknown) => request("POST", `/quilts/${quiltSlug}/submissions`, { body }),
114+
resizeQuiltCanvas: (quiltSlug: string, body: unknown) => request("POST", `/quilts/${quiltSlug}/resize`, { body }),
114115
voteQuiltSubmission: (submissionId: string, body: unknown) => request("POST", `/quilts/submissions/${submissionId}/vote`, { body }),
115116
updateQuiltSubmission: (submissionId: string, body: unknown) => request("PUT", `/quilts/submissions/${submissionId}`, { body }),
116117
acceptQuiltSubmission: (submissionId: string) => request("POST", `/quilts/submissions/${submissionId}/accept`, { }),
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
CREATE TYPE "QuiltSubmissionKind" AS ENUM ('PIXELS', 'RESIZE');
2+
3+
ALTER TABLE "QuiltSubmission"
4+
ADD COLUMN "kind" "QuiltSubmissionKind" NOT NULL DEFAULT 'PIXELS',
5+
ADD COLUMN "canvas_width" INTEGER,
6+
ADD COLUMN "canvas_height" INTEGER,
7+
ADD COLUMN "resize_from_width" INTEGER,
8+
ADD COLUMN "resize_from_height" INTEGER,
9+
ADD COLUMN "resize_offset_x" INTEGER,
10+
ADD COLUMN "resize_offset_y" INTEGER;
11+
12+
UPDATE "QuiltSubmission" submission
13+
SET
14+
"canvas_width" = quilt."width",
15+
"canvas_height" = quilt."height"
16+
FROM "Quilt" quilt
17+
WHERE submission."quilt_id" = quilt."id";

0 commit comments

Comments
 (0)