diff --git a/lib/src/decorators/ignore.ts b/lib/src/decorators/ignore.ts index 62a836a..b582e32 100644 --- a/lib/src/decorators/ignore.ts +++ b/lib/src/decorators/ignore.ts @@ -1,15 +1,7 @@ -import { ModelConstructor } from "../interfaces"; +import "reflect-metadata"; -export const IGNORABLE_FIELDS = new WeakMap>(); +export const IGNORE_KEY = Symbol("secret"); -export function ignore(value: unknown, context: ClassFieldDecoratorContext) { - context.addInitializer(function () { - const ctor = this.constructor as ModelConstructor; - let set = IGNORABLE_FIELDS.get(ctor); - if (!set) { - set = new Set(); - IGNORABLE_FIELDS.set(ctor, set); - } - set.add(context.name as string); - }); +export function ignore(target: any, propertyKey: string) { + Reflect.defineMetadata(IGNORE_KEY, true, target, propertyKey); } diff --git a/lib/src/decorators/immutable.ts b/lib/src/decorators/immutable.ts index d90f781..581cac0 100644 --- a/lib/src/decorators/immutable.ts +++ b/lib/src/decorators/immutable.ts @@ -1,15 +1,5 @@ -import { ModelConstructor } from "../interfaces"; +export const IMMUTABLE_KEY = Symbol("secret"); -export const IMMUTABLE_FIELDS = new WeakMap>(); - -export function immutable(value: unknown, context: ClassFieldDecoratorContext) { - context.addInitializer(function () { - const ctor = this.constructor as ModelConstructor; - let set = IMMUTABLE_FIELDS.get(ctor); - if (!set) { - set = new Set(); - IMMUTABLE_FIELDS.set(ctor, set); - } - set.add(context.name as string); - }); +export function immutable(target: any, propertyKey: string) { + Reflect.defineMetadata(IMMUTABLE_KEY, true, target, propertyKey); } diff --git a/lib/src/decorators/utils.ts b/lib/src/decorators/utils.ts index 93d3ced..75ec614 100644 --- a/lib/src/decorators/utils.ts +++ b/lib/src/decorators/utils.ts @@ -1,17 +1,33 @@ -import { IMMUTABLE_FIELDS } from "./immutable"; -import { IGNORABLE_FIELDS } from "./ignore"; +import { IGNORE_KEY } from "./ignore"; +import { IMMUTABLE_KEY } from "./immutable"; export const getImmutableKeys = (obj: any): string[] => { - const ctor = obj.constructor; - return IMMUTABLE_FIELDS.has(ctor) ? Array.from(IMMUTABLE_FIELDS.get(ctor) as Set) : []; + const prototype = Object.getPrototypeOf(obj); + const immutableKeys: string[] = []; + Object.keys(obj).forEach((key) => { + const isImmutable = Reflect.getMetadata(IMMUTABLE_KEY, prototype, key); + const isSimple = ["string", "number", "boolean", "undefined"].includes(typeof obj[key]) || obj[key] === null; + if (isImmutable || isSimple) { + immutableKeys.push(key); + } + }); + + return immutableKeys; }; export const isImmutableKey = (object: any, key: string) => getImmutableKeys(object).includes(key); export const getIgnorableKeys = (obj: any): string[] => { - const ctor = obj.constructor; + const prototype = Object.getPrototypeOf(obj); + const ignoreKeys: string[] = []; + Object.keys(obj).forEach((key) => { + const isImmutable = Reflect.getMetadata(IGNORE_KEY, prototype, key); + if (isImmutable) { + ignoreKeys.push(key); + } + }); - return IGNORABLE_FIELDS.has(ctor) ? Array.from(IGNORABLE_FIELDS.get(ctor) as Set) : []; + return ignoreKeys; }; export const isIgnorableKey = (object: any, key: string) => getIgnorableKeys(object).includes(key); diff --git a/lib/src/index.ts b/lib/src/index.ts index ddfec6d..50cbc69 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -1,3 +1,4 @@ export * from "./models"; export * from "./decorators"; export * from "./interfaces"; +export * from "./parser"; diff --git a/lib/src/interfaces/fill.ts b/lib/src/interfaces/fill.ts new file mode 100644 index 0000000..957b5fc --- /dev/null +++ b/lib/src/interfaces/fill.ts @@ -0,0 +1,18 @@ +import { IColor } from "@gxc-solutions/colors"; + +export type FillType = "solid" | "texture"; +export type GradientType = "liner" | "radial" | "conic"; + +export interface IBaseFill { + type: GradientType | FillType; +} + +export interface ISolidFill extends IBaseFill { + type: "solid"; + color: IColor; +} + +export interface ITextureFill extends IBaseFill { + type: "texture"; + textureUrl: string; +} diff --git a/lib/src/interfaces/index.ts b/lib/src/interfaces/index.ts index 636af4d..347be64 100644 --- a/lib/src/interfaces/index.ts +++ b/lib/src/interfaces/index.ts @@ -1,8 +1,3 @@ -export type ModelConstructor = new (...args: any[]) => IModel; - -export type TypeOfModel = "collection"; - -export interface IModel { - readonly id: string; - readonly type: TypeOfModel; -} +export * from "./fill"; +export * from "./models"; +export * from "./stroke"; diff --git a/lib/src/interfaces/models.ts b/lib/src/interfaces/models.ts new file mode 100644 index 0000000..534d83a --- /dev/null +++ b/lib/src/interfaces/models.ts @@ -0,0 +1,67 @@ +import { UUID_V4 } from "@gxc-solutions/lett-js"; +import { ISolidFill } from "./fill"; +import { IStroke } from "./stroke"; +import { IPoint } from "@gxc-solutions/math"; + +export type ModelConstructor = new (...args: any[]) => IModel; + +export type TypeOfModel = "collection-model" | "ellipse-model" | "rectangle-model" | "image-model" | "text-object"; + +export interface IModel { + readonly id: UUID_V4; + readonly type: TypeOfModel; +} + +export interface ITransform { + scaleX: number; + scaleY: number; + + translateY: number; + translateX: number; + + skewY: number; + skewX: number; + + rotate: number; +} + +export interface IEllipse { + radiusX: number; + radiusY: number; + center: IPoint; +} + +export interface IRectangle { + width: number; + height: number; + top: number; + left: number; +} + +export interface IEllipseModel extends IModel { + ellipse: IEllipse; + transform: ITransform; + stroke: IStroke; + blendMode: GlobalCompositeOperation; + fill: ISolidFill; + opacity: number; +} + +export interface IRectangleModel extends IModel { + rectangle: IRectangle; + transform: ITransform; + stroke: IStroke; + blendMode: GlobalCompositeOperation; + fill: ISolidFill; + opacity: number; +} + +export interface IImageModel extends IModel { + rectangle: IRectangle; + transform: ITransform; + stroke: IStroke; + blendMode: GlobalCompositeOperation; + fill: ISolidFill; + opacity: number; + url: string; +} diff --git a/lib/src/interfaces/stroke.ts b/lib/src/interfaces/stroke.ts new file mode 100644 index 0000000..d6b8476 --- /dev/null +++ b/lib/src/interfaces/stroke.ts @@ -0,0 +1,7 @@ +import { IColor } from "@gxc-solutions/colors"; + +export interface IStroke { + width: number; + color: IColor; + dash: number[]; +} diff --git a/lib/src/models/index.ts b/lib/src/models/index.ts index 7c36a75..a8c170e 100644 --- a/lib/src/models/index.ts +++ b/lib/src/models/index.ts @@ -1 +1,4 @@ export * from "./collection"; +export * from "./objects"; +export * from "./solid-fill"; +export * from "./stroke"; diff --git a/lib/src/models/objects/base.ts b/lib/src/models/objects/base.ts new file mode 100644 index 0000000..ba19720 --- /dev/null +++ b/lib/src/models/objects/base.ts @@ -0,0 +1,18 @@ +import { ignore } from "../../decorators"; +import { TypeOfModel } from "../../interfaces"; +import { uuidv4, UUID_V4 } from "@gxc-solutions/lett-js/web-api/uuidv4"; + +export abstract class BaseItem { + @ignore readonly id: UUID_V4; + abstract readonly type: TypeOfModel; + name = ""; + // parent: any; + + constructor(id?: UUID_V4) { + this.id = id ?? uuidv4(); + } + + tags: Record = {}; + + abstract copy(): BaseItem; +} diff --git a/lib/src/models/objects/ellipce.ts b/lib/src/models/objects/ellipce.ts new file mode 100644 index 0000000..57faa12 --- /dev/null +++ b/lib/src/models/objects/ellipce.ts @@ -0,0 +1,34 @@ +import { Ellipse, Point, Transform } from "@gxc-solutions/math"; +import { TypeOfModel } from "../../interfaces"; +import { SolidFill } from "../solid-fill"; +import { Stroke } from "../stroke"; +import { BaseItem } from "./base"; +import { ignore } from "../../decorators"; +import { UUID_V4 } from "@gxc-solutions/lett-js"; + +export class EllipseObject extends BaseItem { + @ignore readonly type: TypeOfModel = "ellipse-model"; + + transform = new Transform(); + ellipse = new Ellipse(new Point(0, 0), 0, 0); + + isSelected = false; + + isVisible = true; + isLocked = false; + + opacity = 1; + blendMode: GlobalCompositeOperation = "source-over"; + + stroke = new Stroke(); + fill = new SolidFill(); + + constructor(id?: UUID_V4) { + super(id); + } + + copy(): EllipseObject { + const rect = new EllipseObject(); + return rect; + } +} diff --git a/lib/src/models/objects/image.ts b/lib/src/models/objects/image.ts new file mode 100644 index 0000000..acfb407 --- /dev/null +++ b/lib/src/models/objects/image.ts @@ -0,0 +1,37 @@ +import { Rectangle, Transform } from "@gxc-solutions/math"; +import { ignore } from "../../decorators"; +import { TypeOfModel } from "../../interfaces"; +import { Stroke } from "../stroke"; +import { BaseItem } from "./base"; +import { UUID_V4 } from "@gxc-solutions/lett-js"; +import { SolidFill } from "../solid-fill"; + +export class ImageItem extends BaseItem { + @ignore readonly type: TypeOfModel = "image-model"; + + transform = new Transform(); + rectangle = new Rectangle(); + + stroke = new Stroke(); + fill = new SolidFill(); + + opacity = 1; + blendMode: GlobalCompositeOperation = "source-over"; + + image: { + isLoading: boolean; + image: ImageBitmap; + url: string; + }; + + isLocked = false; + isSelected = false; + + constructor(id?: UUID_V4) { + super(id); + } + + copy(): BaseItem { + throw new Error("Method not implemented."); + } +} diff --git a/lib/src/models/objects/index.ts b/lib/src/models/objects/index.ts new file mode 100644 index 0000000..d368b44 --- /dev/null +++ b/lib/src/models/objects/index.ts @@ -0,0 +1,5 @@ +export * from "./base"; +export * from "./ellipce"; +export * from "./image"; +export * from "./rectangle"; +export * from "./text"; diff --git a/lib/src/models/objects/rectangle.ts b/lib/src/models/objects/rectangle.ts new file mode 100644 index 0000000..033154f --- /dev/null +++ b/lib/src/models/objects/rectangle.ts @@ -0,0 +1,32 @@ +import { Rectangle, Transform } from "@gxc-solutions/math"; +import { ignore } from "../../decorators"; +import { BaseItem } from "./base"; +import { Stroke } from "../stroke"; +import { SolidFill } from "../solid-fill"; +import { UUID_V4 } from "@gxc-solutions/lett-js"; + +export class RectangleItem extends BaseItem { + @ignore readonly type = "rectangle-model"; + + transform = new Transform(); + rectangle = new Rectangle(0, 0, 100, 100); + + isSelected = false; + + isVisible = true; + isLocked = false; + + opacity = 1; + blendMode: GlobalCompositeOperation = "source-over"; + + stroke = new Stroke(); + fill = new SolidFill(); + + constructor(id?: UUID_V4) { + super(id); + } + + copy(): BaseItem { + throw new Error("Not implemented!"); + } +} diff --git a/lib/src/models/objects/text.ts b/lib/src/models/objects/text.ts new file mode 100644 index 0000000..23b0e9a --- /dev/null +++ b/lib/src/models/objects/text.ts @@ -0,0 +1,26 @@ +import { ignore } from "../../decorators"; +import { BaseItem } from "./base"; + +export class TextItem extends BaseItem { + @ignore readonly type = "text-object"; + + isSelected = false; + + // fontSize = 24; + // font = ""; + + // isBold = false; + // isItalic = false; + // isUnderline = false; + // strikeout = false; + + // textAlign: "left" | "center" | "right" | "justify"; + // verticalAlign: "top" | "middle" | "bottom"; + + // lineHeight: number; // ~leading + // letterSpacing: number; // ~tracking + + copy(): BaseItem { + throw new Error("Method not implemented."); + } +} diff --git a/lib/src/models/solid-fill.ts b/lib/src/models/solid-fill.ts new file mode 100644 index 0000000..38d170f --- /dev/null +++ b/lib/src/models/solid-fill.ts @@ -0,0 +1,10 @@ +import { IColor, RgbColor } from "@gxc-solutions/colors"; + +export class SolidFill { + type = "solid"; + color: IColor; + + constructor(color?: IColor) { + this.color = color ?? new RgbColor(255, 255, 255, 1); + } +} diff --git a/lib/src/models/stroke.ts b/lib/src/models/stroke.ts new file mode 100644 index 0000000..76c114d --- /dev/null +++ b/lib/src/models/stroke.ts @@ -0,0 +1,14 @@ +import { IColor, RgbColor } from "@gxc-solutions/colors"; +import { immutable } from "../decorators"; + +export class Stroke { + width: number; + @immutable color: IColor; + @immutable dash: number[]; + + constructor(width?: number, color?: IColor, dash?: number[]) { + this.width = width ?? 0; + this.color = color ?? new RgbColor(0, 0, 0, 0); + this.dash = dash ?? []; + } +} diff --git a/lib/src/package.json b/lib/src/package.json index 6873169..f664772 100644 --- a/lib/src/package.json +++ b/lib/src/package.json @@ -1,10 +1,16 @@ { "name": "@gxc-solutions/model-base", - "version": "0.0.1", + "version": "0.0.2", "main": "index.js", "author": "GXC Solutions", "publishConfig": { "access": "public", "registry": "https://npm.gxc-solutions.ru" + }, + "peerDependencies": { + "@gxc-solutions/colors": "0.0.1", + "@gxc-solutions/lett-js": "^1.0.1", + "@gxc-solutions/math": "^0.0.2", + "reflect-metadata": "^0.2.2" } } diff --git a/lib/src/parser.ts b/lib/src/parser.ts new file mode 100644 index 0000000..ed0169b --- /dev/null +++ b/lib/src/parser.ts @@ -0,0 +1,104 @@ +import { _switch } from "@gxc-solutions/lett-js"; +import { + FillType, + GradientType, + IBaseFill, + IEllipseModel, + IImageModel, + IModel, + IRectangle, + IRectangleModel, + ISolidFill, + IStroke, + ITransform, + TypeOfModel, +} from "./interfaces"; +import { EllipseObject } from "./models/objects/ellipce"; +import { ImageItem } from "./models/objects/image"; +import { Stroke } from "./models/stroke"; +import { SolidFill } from "./models/solid-fill"; +import { Ellipse, Rectangle, Transform } from "@gxc-solutions/math"; +import { RectangleItem } from "./models/objects/rectangle"; + +export class Parser { + constructor() {} + + parse() {} + + parseModel(objects: IModel[]) { + return objects.map((object) => + _switch(object.type) + .case("ellipse-model", () => { + const ellipseObj = object as IEllipseModel; + const ellipse = new EllipseObject(ellipseObj.id); + + ellipse.ellipse = new Ellipse(ellipseObj.ellipse.center, ellipseObj.ellipse.radiusX, ellipseObj.ellipse.radiusY); + ellipse.blendMode = ellipseObj.blendMode; + ellipse.opacity = ellipseObj.opacity; + ellipse.stroke = this.parseStroke(ellipseObj.stroke); + ellipse.fill = this.parseFill(ellipseObj.fill); + ellipse.transform = this.parseTransform(ellipseObj.transform); + return ellipse; + }) + .case("image-model", () => { + const imageObj = object as IImageModel; + const image = new ImageItem(imageObj.id); + + image.rectangle = this.parseRectangle(imageObj.rectangle); + image.blendMode = imageObj.blendMode; + image.opacity = imageObj.opacity; + image.stroke = this.parseStroke(imageObj.stroke); + image.fill = this.parseFill(imageObj.fill); + image.transform = this.parseTransform(imageObj.transform); + image.image = { + isLoading: true, + image: null, + url: imageObj.url, + }; + return image; + }) + .case("rectangle-model", () => { + const rectangleObj = object as IRectangleModel; + const rectangle = new RectangleItem(rectangleObj.id); + + rectangle.rectangle = this.parseRectangle(rectangleObj.rectangle); + rectangle.blendMode = rectangleObj.blendMode; + rectangle.opacity = rectangleObj.opacity; + rectangle.stroke = this.parseStroke(rectangleObj.stroke); + rectangle.fill = this.parseFill(rectangleObj.fill); + rectangle.transform = this.parseTransform(rectangleObj.transform); + return rectangle; + }) + .result(), + ); + } + + parseStroke(strokeObj: IStroke) { + return new Stroke(strokeObj.width, strokeObj.color, strokeObj.dash); + } + + parseFill(fillObj: IBaseFill) { + return _switch(fillObj.type) + .case("solid", () => { + const solidFillObj = fillObj as ISolidFill; + return new SolidFill(solidFillObj.color); + }) + .result(); + } + + parseTransform(transformObj: ITransform) { + const transform = new Transform(); + transform.rotate = transformObj.rotate; + transform.scaleX = transformObj.scaleX; + transform.scaleY = transformObj.scaleY; + transform.skewX = transformObj.skewX; + transform.skewY = transformObj.skewY; + transform.translateX = transformObj.translateX; + transform.translateY = transformObj.translateY; + return transform; + } + + parseRectangle(rectangleObj: IRectangle) { + return new Rectangle(rectangleObj.top, rectangleObj.left, rectangleObj.width, rectangleObj.height); + } +} diff --git a/package-lock.json b/package-lock.json index e92ee18..d10ec9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,12 @@ "name": "template-of-lib-repo", "version": "0.0.0", "license": "ISC", + "dependencies": { + "@gxc-solutions/colors": "0.0.1", + "@gxc-solutions/lett-js": "^1.0.1", + "@gxc-solutions/math": "^0.0.2", + "reflect-metadata": "^0.2.2" + }, "devDependencies": { "@eslint/eslintrc": "^3.3.4", "@eslint/js": "^10.0.1", @@ -691,6 +697,21 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@gxc-solutions/colors": { + "version": "0.0.1", + "resolved": "https://npm.gxc-solutions.ru/@gxc-solutions/colors/-/colors-0.0.1.tgz", + "integrity": "sha512-KZ1T7V9YYxqf+jFpKhU7wxDqT14n5uqUFn2zGmVtwr4SjCWsKpq6c3eOT9OzPoQW9C8K02RzkuyBddbRowX1vg==" + }, + "node_modules/@gxc-solutions/lett-js": { + "version": "1.0.1", + "resolved": "https://npm.gxc-solutions.ru/@gxc-solutions/lett-js/-/lett-js-1.0.1.tgz", + "integrity": "sha512-wmRXERIrb3md5G3OI4dEBQmzh005meLm7EMK5K5U1eFX+GjuhAGSm/Wi/eag0LQmjy+L1R9koxsuhzN2vTfigA==" + }, + "node_modules/@gxc-solutions/math": { + "version": "0.0.2", + "resolved": "https://npm.gxc-solutions.ru/@gxc-solutions/math/-/math-0.0.2.tgz", + "integrity": "sha512-R6zYvbspis+XoHZ7lwyVLAUGZkJwivbeo94VtN7cSZzOA86AgVev+UQeHt9S1Ua4d1FwgxYVasck1/l5WXR7CQ==" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2256,6 +2277,12 @@ "node": ">=6" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/package.json b/package.json index 23fa27e..8e41497 100644 --- a/package.json +++ b/package.json @@ -29,5 +29,11 @@ "rimraf": "^6.0.1", "typescript": "^5.9.3", "vite": "^7.3.1" + }, + "dependencies": { + "@gxc-solutions/colors": "0.0.1", + "@gxc-solutions/lett-js": "^1.0.1", + "@gxc-solutions/math": "^0.0.2", + "reflect-metadata": "^0.2.2" } } diff --git a/tsconfig.json b/tsconfig.json index 6f67808..f9d4306 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "esModuleInterop": true, "moduleResolution": "node", "skipLibCheck": true, - "experimentalDecorators": false, + "experimentalDecorators": true, "strictNullChecks": false, "outDir": "./dist" },