mirror of
https://github.com/YuzuZensai/Pien-Studio.git
synced 2026-09-02 14:18:35 +00:00
✨ feat: initial app
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@pien-studio/types",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^4.4.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DeviceSessionSchema, ProjectFileSchema, ProjectSchema } from "./index";
|
||||
|
||||
describe("types schemas", () => {
|
||||
it("accepts valid device session payload", () => {
|
||||
const result = DeviceSessionSchema.safeParse({
|
||||
deviceId: "abcd1234",
|
||||
locale: "ja",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects unknown locale", () => {
|
||||
const result = DeviceSessionSchema.safeParse({
|
||||
deviceId: "abcd1234",
|
||||
locale: "fr",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("validates project shape", () => {
|
||||
const result = ProjectSchema.safeParse({
|
||||
id: "p1",
|
||||
title: "test",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
aspectRatio: "4:5",
|
||||
canvas: { width: 1080, height: 1350, unit: "px" },
|
||||
layers: [],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("validates versioned project file envelope", () => {
|
||||
const now = new Date().toISOString();
|
||||
const result = ProjectFileSchema.safeParse({
|
||||
format: "pien.project",
|
||||
version: 1,
|
||||
exportedAt: now,
|
||||
app: { name: "pien.studio", platform: "web" },
|
||||
project: {
|
||||
id: "p1",
|
||||
title: "test",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
aspectRatio: "4:5",
|
||||
canvas: { width: 1080, height: 1350, unit: "px" },
|
||||
layers: [],
|
||||
},
|
||||
assets: [],
|
||||
history: { checkpointCount: 0 },
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const LayerTypeSchema = z.enum(["image", "text", "sticker"]);
|
||||
|
||||
export const FaceBlurMethodSchema = z.enum(["gaussian", "pixelate", "censor"]);
|
||||
|
||||
export const FaceBlurRegionSchema = z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
sourceWidth: z.number().optional(),
|
||||
sourceHeight: z.number().optional(),
|
||||
censorColor: z.string().optional(),
|
||||
});
|
||||
|
||||
export const FaceBlurSettingsSchema = z.object({
|
||||
method: FaceBlurMethodSchema,
|
||||
amount: z.number().int().min(4).max(40),
|
||||
regions: z.array(FaceBlurRegionSchema),
|
||||
censorColor: z.string().optional(),
|
||||
});
|
||||
|
||||
export const LayerSchema = z.object({
|
||||
id: z.string(),
|
||||
type: LayerTypeSchema,
|
||||
name: z.string().optional(),
|
||||
assetId: z.string().optional(),
|
||||
sourceUri: z.string().optional(),
|
||||
faceBlur: FaceBlurSettingsSchema.optional(),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
width: z.number().optional(),
|
||||
height: z.number().optional(),
|
||||
scale: z.number().default(1),
|
||||
rotation: z.number().default(0),
|
||||
opacity: z.number().min(0).max(1).default(1),
|
||||
});
|
||||
|
||||
export const AspectRatioSchema = z.enum([
|
||||
"1:1", // square feed
|
||||
"4:5", // portrait 4:5
|
||||
"9:16", // story / vertical
|
||||
"16:9", // widescreen
|
||||
"4:3", // classic photo
|
||||
"3:2", // landscape photo
|
||||
"free", // custom
|
||||
]);
|
||||
|
||||
export type AspectRatio = z.infer<typeof AspectRatioSchema>;
|
||||
|
||||
export const CanvasSizeSchema = z.object({
|
||||
width: z.number().int().positive(),
|
||||
height: z.number().int().positive(),
|
||||
unit: z.enum(["px", "in", "cm"]).default("px"),
|
||||
});
|
||||
|
||||
export const ProjectSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
canvas: CanvasSizeSchema,
|
||||
aspectRatio: AspectRatioSchema,
|
||||
layers: z.array(LayerSchema),
|
||||
});
|
||||
|
||||
export const PresetAspectRatioSchema = z.object({
|
||||
label: z.string(),
|
||||
value: AspectRatioSchema,
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
});
|
||||
|
||||
export type PresetAspectRatio = z.infer<typeof PresetAspectRatioSchema>;
|
||||
|
||||
export const PRESET_CANVAS_SIZES: PresetAspectRatio[] = [
|
||||
{ label: "Square (1:1)", value: "1:1", width: 1080, height: 1080 },
|
||||
{ label: "Portrait (4:5)", value: "4:5", width: 1080, height: 1350 },
|
||||
{ label: "Story (9:16)", value: "9:16", width: 1080, height: 1920 },
|
||||
{ label: "Widescreen (16:9)", value: "16:9", width: 1920, height: 1080 },
|
||||
{ label: "Photo (4:3)", value: "4:3", width: 1440, height: 1080 },
|
||||
{ label: "Classic (3:2)", value: "3:2", width: 1620, height: 1080 },
|
||||
];
|
||||
|
||||
export const DeviceSessionSchema = z.object({
|
||||
deviceId: z.string().min(4),
|
||||
locale: z.enum(["en", "th", "ja"]),
|
||||
});
|
||||
|
||||
export const ProjectFileV1Schema = z.object({
|
||||
format: z.literal("pien.project"),
|
||||
version: z.literal(1),
|
||||
exportedAt: z.string(),
|
||||
app: z.object({
|
||||
name: z.literal("pien.studio"),
|
||||
platform: z.enum(["web", "mobile", "desktop"]).default("web"),
|
||||
}),
|
||||
project: ProjectSchema,
|
||||
assets: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
kind: z.enum(["image", "sticker", "font", "frame"]),
|
||||
name: z.string(),
|
||||
uri: z.string(),
|
||||
checksum: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
history: z.object({
|
||||
checkpointCount: z.number().int().nonnegative().default(0),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ProjectFileSchema = ProjectFileV1Schema;
|
||||
|
||||
export type Project = z.infer<typeof ProjectSchema>;
|
||||
export type Layer = z.infer<typeof LayerSchema>;
|
||||
export type LayerType = z.infer<typeof LayerTypeSchema>;
|
||||
export type FaceBlurMethod = z.infer<typeof FaceBlurMethodSchema>;
|
||||
export type FaceBlurRegion = z.infer<typeof FaceBlurRegionSchema>;
|
||||
export type FaceBlurSettings = z.infer<typeof FaceBlurSettingsSchema>;
|
||||
export type ProjectFile = z.infer<typeof ProjectFileSchema>;
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
include: ["**/*.test.ts"],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user