♻️ refactor!: massive refactor

This commit is contained in:
2026-05-31 03:04:30 +07:00
parent c1f12c7201
commit df3c944547
86 changed files with 3691 additions and 1129 deletions
+3
View File
@@ -5,6 +5,9 @@
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"test": "vitest run",
"typecheck": "tsc --noEmit"
+2 -18
View File
@@ -1,23 +1,7 @@
import { describe, expect, it } from "vitest";
import { DeviceSessionSchema, ProjectFileSchema, ProjectSchema } from "./index";
import { 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",
@@ -35,7 +19,7 @@ describe("types schemas", () => {
const now = new Date().toISOString();
const result = ProjectFileSchema.safeParse({
format: "pien.project",
version: 1,
version: 2,
exportedAt: now,
app: { name: "pien.studio", platform: "web" },
project: {
+106 -19
View File
@@ -2,6 +2,12 @@ import { z } from "zod";
export const LayerTypeSchema = z.enum(["raster", "text", "sticker"]);
export const AssetRefSchema = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("inline"), uri: z.string() }),
z.object({ kind: z.literal("stored"), id: z.string() }),
z.object({ kind: z.literal("remote"), uri: z.string() }),
]);
export const FaceBlurMethodSchema = z.enum(["gaussian", "pixelate", "censor"]);
export const FaceBlurRegionSchema = z.object({
@@ -25,31 +31,55 @@ export const FaceBlurEffectSchema = z.object({
export const LayerEffectSchema = FaceBlurEffectSchema;
export const LayerSchema = z.object({
const BaseLayerSchema = z.object({
id: z.string(),
type: LayerTypeSchema,
name: z.string().optional(),
assetId: z.string().optional(),
sourceUri: z.string().optional(),
effects: z.array(LayerEffectSchema).default([]),
visible: z.boolean().default(true),
x: z.number(),
y: z.number(),
width: z.number().optional(),
height: z.number().optional(),
width: z.number(),
height: z.number(),
scale: z.number().default(1),
rotation: z.number().default(0),
opacity: z.number().min(0).max(1).default(1),
});
export const RasterLayerSchema = BaseLayerSchema.extend({
type: z.literal("raster"),
asset: AssetRefSchema.nullable(),
runtimeSourceUri: z.string().optional(),
});
export const TextLayerSchema = BaseLayerSchema.extend({
type: z.literal("text"),
text: z.string(),
fontFamily: z.string().default("system-ui"),
fontSize: z.number().positive().default(48),
color: z.string().default("#111827"),
});
export const StickerLayerSchema = BaseLayerSchema.extend({
type: z.literal("sticker"),
asset: AssetRefSchema.nullable(),
runtimeSourceUri: z.string().optional(),
stickerId: z.string().optional(),
});
export const LayerSchema = z.discriminatedUnion("type", [
RasterLayerSchema,
TextLayerSchema,
StickerLayerSchema,
]);
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
"1:1",
"4:5",
"9:16",
"16:9",
"4:3",
"3:2",
"free",
]);
export type AspectRatio = z.infer<typeof AspectRatioSchema>;
@@ -88,14 +118,9 @@ export const PRESET_CANVAS_SIZES: PresetAspectRatio[] = [
{ 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),
version: z.literal(2),
exportedAt: z.string(),
app: z.object({
name: z.literal("pien.studio"),
@@ -120,9 +145,71 @@ export const ProjectFileSchema = ProjectFileV1Schema;
export type Project = z.infer<typeof ProjectSchema>;
export type Layer = z.infer<typeof LayerSchema>;
export type RasterLayer = z.infer<typeof RasterLayerSchema>;
export type TextLayer = z.infer<typeof TextLayerSchema>;
export type StickerLayer = z.infer<typeof StickerLayerSchema>;
export type LayerType = z.infer<typeof LayerTypeSchema>;
export type AssetRef = z.infer<typeof AssetRefSchema>;
export type LayerEffect = z.infer<typeof LayerEffectSchema>;
export type FaceBlurMethod = z.infer<typeof FaceBlurMethodSchema>;
export type FaceBlurRegion = z.infer<typeof FaceBlurRegionSchema>;
export type FaceBlurEffect = z.infer<typeof FaceBlurEffectSchema>;
export type ProjectFile = z.infer<typeof ProjectFileSchema>;
export function getAssetRefId(
asset: AssetRef | null | undefined,
): string | undefined {
return asset?.kind === "stored" ? asset.id : undefined;
}
export function getLayerAssetRef(layer: Layer): AssetRef | null | undefined {
return layer.type === "raster" || layer.type === "sticker"
? layer.asset
: undefined;
}
export function getLayerRuntimeSource(layer: Layer): string | undefined {
if (layer.type !== "raster" && layer.type !== "sticker") return undefined;
return (
layer.runtimeSourceUri ??
(layer.asset?.kind === "inline" || layer.asset?.kind === "remote"
? layer.asset.uri
: undefined)
);
}
export function normalizeLayerEffect(effect: LayerEffect): LayerEffect {
if (effect.kind === "face-blur") {
return {
...effect,
enabled: effect.enabled ?? true,
amount: Math.max(4, Math.min(40, Math.round(effect.amount))),
regions: Array.isArray(effect.regions) ? effect.regions : [],
};
}
return effect;
}
export function normalizeProject(project: Project): Project {
return {
...project,
canvas: {
width: Math.max(1, Math.round(project.canvas.width)),
height: Math.max(1, Math.round(project.canvas.height)),
unit: project.canvas.unit,
},
layers: project.layers.map((layer) => ({
...layer,
width: Math.max(1, Math.round(layer.width)),
height: Math.max(1, Math.round(layer.height)),
scale: Number.isFinite(layer.scale) ? layer.scale : 1,
rotation: Number.isFinite(layer.rotation) ? layer.rotation : 0,
opacity: Number.isFinite(layer.opacity)
? Math.max(0, Math.min(1, layer.opacity))
: 1,
effects: Array.isArray(layer.effects)
? layer.effects.map(normalizeLayerEffect)
: [],
})),
};
}