1- import { QuiltSubmissionStatus } from "@prisma/client" ;
1+ import { QuiltSubmissionKind , QuiltSubmissionStatus } from "@prisma/client" ;
22import { z } from "zod" ;
33
44import { 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+
4451export const quiltSlugParamsSchema = z . object ( {
4552 quiltSlug : z . string ( ) . trim ( ) . min ( 1 ) ,
4653} ) ;
@@ -61,7 +68,14 @@ type QuiltPixel = z.infer<typeof quiltPixelSchema>;
6168
6269type 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(
134155function 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
158253async 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+
441621export 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 ;
0 commit comments