mirror of
https://github.com/YuzuZensai/Pien-Studio.git
synced 2026-09-02 14:18:35 +00:00
♻️ refactor!: massive refactor
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
@@ -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)
|
||||
: [],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user