diff --git a/src/texts/en.ts b/src/texts/en.ts index 15f52e8..fcd2554 100644 --- a/src/texts/en.ts +++ b/src/texts/en.ts @@ -97,6 +97,7 @@ export const texts = { zoomIn: 'Zoom In', zoomOut: 'Zoom Out', webGL: 'Web GL', - webGLHints: 'Use WebGL as alternative renderer. Requires restart', + webGLHints1: 'Use WebGL as alternative renderer.', + webGLHints2: 'Require manual refresh (F5)', }, }; diff --git a/src/wireframes/components/properties/Experimental.tsx b/src/wireframes/components/properties/Experimental.tsx index d707c47..c6b7485 100644 --- a/src/wireframes/components/properties/Experimental.tsx +++ b/src/wireframes/components/properties/Experimental.tsx @@ -21,14 +21,19 @@ export const Experimental = () => { }, [dispatch]); return ( - - {texts.common.webGL} - - + <> + + {texts.common.webGLHints1} + {texts.common.webGLHints2} + - {texts.common.webGLHints} - - + + {texts.common.webGL} + + + + + > ); }; \ No newline at end of file diff --git a/src/wireframes/engine/pixi/engine.ts b/src/wireframes/engine/pixi/engine.ts index 072b964..e587260 100644 --- a/src/wireframes/engine/pixi/engine.ts +++ b/src/wireframes/engine/pixi/engine.ts @@ -103,7 +103,7 @@ export class PixiEngine implements Engine { const engineEvent = new EngineMouseEvent( event, - new Vec2(x, y)); + new Vec2(Math.round(x), Math.round(y))); return engineEvent; }; @@ -135,7 +135,7 @@ export class PixiEngine implements Engine { const engineEvent = new EngineHitEvent( event, - new Vec2(x, y), + new Vec2(Math.round(x), Math.round(y)), eventObject, eventItem, hit); diff --git a/src/wireframes/engine/pixi/renderer.ts b/src/wireframes/engine/pixi/renderer.ts index 7b92010..408d2a2 100644 --- a/src/wireframes/engine/pixi/renderer.ts +++ b/src/wireframes/engine/pixi/renderer.ts @@ -8,12 +8,48 @@ /* eslint-disable quote-props */ import { marked } from 'marked'; -import { Assets, Container, ContainerChild, Graphics, GraphicsPath, HTMLText, Sprite, StrokeStyle, TextStyle, Texture, ViewContainer } from 'pixi.js'; +import { Assets, Container, ContainerChild, Graphics, GraphicsPath, HTMLText, Sprite, StrokeStyle, TextStyle, ViewContainer } from 'pixi.js'; import { Rect2, TextMeasurer, Types } from '@app/core/utils'; import { RendererColor, RendererOpacity, RendererText, RendererWidth, Shape, ShapeProperties, ShapePropertiesFunc, ShapeRenderer, ShapeRendererFunc, TextConfig, TextDecoration } from '@app/wireframes/interface'; -import { getBackgroundColor, getFontFamily, getFontSize, getForegroundColor, getOpacity, getStrokeColor, getStrokeWidth, getText, getTextAlignment } from './../shared'; +import { getBackgroundColor, getFontFamily, getFontSize, getForegroundColor, getOpacity, getStrokeColor, getStrokeWidth, getText, getTextAlignment, getValue, setValue } from './../shared'; import { PixiHelper } from './utils'; +type GraphicsType = + 'Ellipse' | + 'Path' | + 'Rect' | + 'RoundedRectangleBottom' | + 'RoundedRectangleLeft' | + 'RoundedRectangleRight' | + 'RoundedRectangleTop'; + +type TextType = 'Single' | 'Multi'; + +type Properties = { + graphics?: { + bounds: Rect2; + fill: string; + path?: string; + radius: number; + stroke: StrokeStyle; + type: GraphicsType; + }; + opacity: number; + sprite?: { + bounds: Rect2; + ratio: boolean; + source: string; + }; + textAlign?: { + align: TextStyle['align']; + bounds: Rect2; + type: TextType; + }; + textContent?: { text?: string; markdown?: boolean; underline?: boolean }; + textStyle?: Partial; +}; + +/* type Properties = { fill?: string | null; bounds: Rect2; @@ -26,7 +62,7 @@ type Properties = { textContent: { text?: string; markdown?: boolean; underline?: boolean }; textStyle: Partial; textMode?: 'Single' | 'Multi'; -}; +};*/ type Test = (existing: ContainerChild) => boolean; @@ -107,100 +143,139 @@ export class PixiRenderer implements ShapeRenderer { public rectangle(strokeWidth: RendererWidth, radius: number, bounds: Rect2, properties?: ShapePropertiesFunc) { return this.new(IS_GRAPHICS, FACTORY_GRAPHICS, { - bounds, - graphicsShape: 'Rect', - radius, - stroke: { width: getStrokeWidth(strokeWidth) }, + graphics: { + bounds, + fill: 'transparent', + radius, + stroke: { width: getStrokeWidth(strokeWidth) }, + type: 'Rect', + }, }, properties); } public ellipse(strokeWidth: RendererWidth, bounds: Rect2, properties?: ShapePropertiesFunc) { return this.new(IS_GRAPHICS, FACTORY_GRAPHICS, { - bounds, - graphicsShape: 'Ellipse', - stroke: { width: getStrokeWidth(strokeWidth) }, + graphics: { + bounds, + fill: 'transparent', + radius: 0, + stroke: { width: getStrokeWidth(strokeWidth) }, + type: 'Ellipse', + }, }, properties); } public roundedRectangleLeft(strokeWidth: RendererWidth, radius: number, bounds: Rect2, properties?: ShapePropertiesFunc) { return this.new(IS_GRAPHICS, FACTORY_GRAPHICS, { - bounds, - graphicsShape: 'RoundedRectangleLeft', - radius, - stroke: { width: getStrokeWidth(strokeWidth) }, + graphics: { + bounds, + fill: 'transparent', + radius, + stroke: { width: getStrokeWidth(strokeWidth) }, + type: 'RoundedRectangleLeft', + }, }, properties); } public roundedRectangleRight(strokeWidth: RendererWidth, radius: number, bounds: Rect2, properties?: ShapePropertiesFunc) { return this.new(IS_GRAPHICS, FACTORY_GRAPHICS, { - bounds, - graphicsShape: 'RoundedRectangleRight', - radius, - stroke: { width: getStrokeWidth(strokeWidth) }, + graphics: { + bounds, + fill: 'transparent', + radius, + stroke: { width: getStrokeWidth(strokeWidth) }, + type: 'RoundedRectangleRight', + }, }, properties); } public roundedRectangleTop(strokeWidth: RendererWidth, radius: number, bounds: Rect2, properties?: ShapePropertiesFunc) { return this.new(IS_GRAPHICS, FACTORY_GRAPHICS, { - bounds, - graphicsShape: 'RoundedRectangleTop', - radius, - stroke: { width: getStrokeWidth(strokeWidth) }, + graphics: { + bounds, + fill: 'transparent', + radius, + stroke: { width: getStrokeWidth(strokeWidth) }, + type: 'RoundedRectangleTop', + }, }, properties); } public roundedRectangleBottom(strokeWidth: RendererWidth, radius: number, bounds: Rect2, properties?: ShapePropertiesFunc) { return this.new(IS_GRAPHICS, FACTORY_GRAPHICS, { - bounds, - graphicsShape: 'RoundedRectangleBottom', - radius, - stroke: { width: getStrokeWidth(strokeWidth) }, + graphics: { + bounds, + fill: 'transparent', + radius, + stroke: { width: getStrokeWidth(strokeWidth) }, + type: 'RoundedRectangleBottom', + }, }, properties); } public path(strokeWidth: RendererWidth, path: string, properties?: ShapePropertiesFunc) { return this.new(IS_GRAPHICS, FACTORY_GRAPHICS, { - graphicsPath: path, - graphicsShape: 'Path', - stroke: { width: getStrokeWidth(strokeWidth) }, + graphics: { + bounds: Rect2.EMPTY, + fill: 'transparent', + path, + radius: 0, + stroke: { width: getStrokeWidth(strokeWidth) }, + type: 'Path', + }, }, properties); } public text(config: RendererText, bounds: Rect2, properties?: ShapePropertiesFunc, allowMarkdown?: boolean) { const fontSize = getFontSize(config); + const align = getTextAlignment(config) as any; - return this.new(IS_TEXT, FACTORY_TEXT, { - bounds, + return this.new(IS_TEXT, FACTORY_TEXT, { + textAlign: { + align, + bounds, + type: 'Single', + }, + textContent: { text: getText(config), markdown: allowMarkdown }, textStyle: { - align: getTextAlignment(config) as any, - fontSize, + align, fontFamily: getFontFamily(config), + fontSize, lineHeight: fontSize * 1.5, + wordWrap: false, }, - textMode: 'Single', - textContent: { text: getText(config), markdown: allowMarkdown }, }, properties); } public textMultiline(config: RendererText, bounds: Rect2, properties?: ShapePropertiesFunc, allowMarkdown?: boolean) { const fontSize = getFontSize(config); + const align = getTextAlignment(config) as any; - return this.new(IS_TEXT, FACTORY_TEXT, { - bounds, + return this.new(IS_TEXT, FACTORY_TEXT, { + textAlign: { + align, + bounds, + type: 'Multi', + }, + textContent: { text: getText(config), markdown: allowMarkdown }, textStyle: { - align: getTextAlignment(config) as any, - fontSize, + align, fontFamily: getFontFamily(config), + fontSize, lineHeight: fontSize * 1.5, wordWrap: true, }, - textMode: 'Multi', - textContent: { text: getText(config), markdown: allowMarkdown }, }, properties); } public raster(source: string, bounds: Rect2, preserveAspectRatio?: boolean, properties?: ShapePropertiesFunc) { - return this.new(IS_SPRITE, FACTORY_SPRITE, { bounds, raster: { source, keepRatio: preserveAspectRatio } }, properties); + return this.new(IS_SPRITE, FACTORY_SPRITE, { + sprite: { + bounds, + ratio: !!preserveAspectRatio, + source, + }, + }, properties); } public group(items: ShapeRendererFunc, clip?: ShapeRendererFunc, properties?: ShapePropertiesFunc) { @@ -241,16 +316,7 @@ export class PixiRenderer implements ShapeRenderer { // Create new properties to ensure to unset properties that are not set anymore. const properties: Properties = { - bounds: Rect2.ZERO, - fill: 'transparent', - graphicsPath: null, - graphicsShape: null, opacity: 1, - radius: 0, - raster: null, - stroke: {}, - textContent: {}, - textStyle: {}, }; Object.assign(properties, initialProperties); @@ -275,11 +341,16 @@ export class PixiRenderer implements ShapeRenderer { const element = factory(properties, existing as T); if (this.clipping) { - this.currentContainer.mask = element; - + if (this.currentContainer.mask !== element) { + this.currentContainer.mask = element; + } + this.wasClipped = true; - } else if (element !== existing) { - this.currentContainer.addChildAt(element, this.currentIndex); + } else { + if (element !== existing) { + this.currentContainer.addChildAt(element, this.currentIndex); + } + this.currentIndex++; } @@ -303,34 +374,33 @@ export class PixiRenderer implements ShapeRenderer { } } -type Setters = { - [P in keyof T]?: (value: T[P], element: ContainerChild, all: T) => void; +type Setters = { + [P in keyof T]?: (value: T[P], element: E) => void; }; class PropertiesSetter implements ShapeProperties { - private static readonly SETTERS: Setters = { - opacity: (value, element) => { - element.alpha = value; - }, - graphicsShape: (value, element, p) => { + private static readonly SETTERS = Object.entries({ + graphics: (value, element) => { if (!value || !Types.is(element, Graphics)) { return; } - const { bounds, graphicsPath: path, graphicsShape: shape, radius } = p; - if (shape === 'Rect' && radius > 0) { + const { bounds, fill, path, radius, stroke, type } = value; + + element.clear(); + if (type === 'Rect' && radius > 0) { element.roundRect(bounds.x, bounds.y, bounds.w, bounds.h, radius); - } else if (shape === 'Rect') { + } else if (type === 'Rect') { element.rect(bounds.x, bounds.y, bounds.w, bounds.h); - } else if (shape === 'Path' && path) { + } else if (type === 'Path' && path) { element.path(new GraphicsPath(path)); - } else if (shape === 'RoundedRectangleLeft') { + } else if (type === 'RoundedRectangleLeft') { PixiHelper.roundedRectangleLeft(element, bounds, radius); - } else if (shape === 'RoundedRectangleRight') { + } else if (type === 'RoundedRectangleRight') { PixiHelper.roundedRectangleRight(element, bounds, radius); - } else if (shape === 'RoundedRectangleTop') { + } else if (type === 'RoundedRectangleTop') { PixiHelper.roundedRectangleTop(element, bounds, radius); - } else if (shape === 'RoundedRectangleBottom') { + } else if (type === 'RoundedRectangleBottom') { PixiHelper.roundedRectangleBottom(element, bounds, radius); } else { const rx = bounds.w * 0.5; @@ -339,80 +409,75 @@ class PropertiesSetter implements ShapeProperties { const cy = bounds.y + ry; element.ellipse(cx, cy, rx, ry); } - - }, - fill: (value, element) => { - if (!Types.is(element, Graphics)) { - return; - } - - element.fill(value as any); + element.fill(fill); + element.stroke({ alignment: 1, ...stroke }); }, - stroke: (value, element) => { - if (!Types.is(element, Graphics)) { - return; - } - - element.stroke({ alignment: 1, ...value }); - }, - bounds: (value, element) => { - if (!Types.is(element, Sprite)) { - return; - } - - element.width = value.w; - element.height = value.h; + opacity: (value, element) => { + element.alpha = value; }, - raster: (value, element, p) => { - if (!Types.is(element, Sprite)) { + sprite: async (value, element) => { + if (!value || !Types.is(element, Sprite)) { return; } - (element as any)['source'] = value; - if (!value) { - element.texture = null!; + const { bounds, ratio, source: src } = value; + if (!src) { return; } - const loaded = Assets.load({ - src: value.source, - loadParser: 'loadTextures', - }); + function align(target: Sprite, bounds: Rect2, ratio: boolean) { + const texture = target.texture; - loaded.then((texture: Texture) => { - let lastRequest = (element as any)['source'] as Properties['raster'] | undefined; - - if (lastRequest?.source !== value.source) { + if (!texture) { return; } - if (value?.keepRatio && texture) { + let x = 0; + let y = 0; + let w = bounds.w; + let h = bounds.h; + + if (ratio && texture) { const size = element.getSize(); const ratioElement = size.width / size.height; const ratioImage = texture.width / texture.height; - let w = 0; - let h = 0; if (ratioImage > ratioElement) { w = size.width; h = size.width / ratioImage; - element.y = p.bounds.y + (size.height - h) * 0.5; + y = bounds.y + (size.height - h) * 0.5; } else { w = size.height * ratioImage; h = size.height; - element.x = p.bounds.x + (size.width - w) * 0.5; + x = bounds.x + (size.width - w) * 0.5; } - - element.width = w; - element.height = h; } - element.texture = texture; - }); + element.x = x; + element.y = y; + element.width = w; + element.height = h; + } + + let lastValue = getValue(element, 'values'); + if (lastValue?.source === value.source) { + align(element, bounds, ratio); + return; + } + + const texture = await Assets.load({ src, loadParser: 'loadTextures' }); + + lastValue = getValue(element, 'values'); + if (lastValue !== value) { + return; + } + + element.texture = texture; + align(element, bounds, ratio); }, textContent: (value, element) => { - if (!Types.is(element, HTMLText)) { + if (!value || !Types.is(element, HTMLText)) { return; } @@ -426,72 +491,60 @@ class PropertiesSetter implements ShapeProperties { } element.text = textOrHtml || ''; + setValue(element, 'size', null); }, - textStyle: (value, element) => { - if (!Types.is(element, HTMLText)) { + textStyle: (value, element) => { + if (!value || !Types.is(element, HTMLText)) { return; } - + element.style = { padding: 10, ...value }; + setValue(element, 'size', null); }, - textMode: (value, element, p) => { - if (!Types.is(element, HTMLText)) { + textAlign: (value, element) => { + if (!value || !Types.is(element, HTMLText)) { return; } - const { bounds } = p; + const { align, bounds, type } = value; const mask = element.mask as Graphics; mask?.clear(); mask?.rect(0, 0, bounds.w, bounds.h).fill(0xffffff); - const size = element.getSize(); + const size = getValue(element, 'size', () => element.getSize()); let x = bounds.x; let y = bounds.y; - if (value === 'Single') { + if (type === 'Single') { y += Math.max((bounds.h - size.height) * 0.5, 0); } - if (element.style.align === 'center') { + if (align === 'center') { x += Math.max((bounds.w - size.width) * 0.5, 0); - } else if (element.style.align === 'right') { + } else if (align === 'right') { x += bounds.w - size.width; } element.position = { x, y }; }, - }; - - private static SETTERS_ENTRIES = Object.entries(this.SETTERS); + } as Setters); private properties: Properties = null!; public static readonly INSTANCE = new PropertiesSetter(); public apply(element: ContainerChild) { - const oldProperties = (element as any)['properties'] || {} as any; + const oldProperties = getValue(element, 'properties', () => ({} as Record)); - if (Types.is(element, Graphics)) { - if (Types.equals(this.properties, oldProperties)) { - return; - } + const properties = this.properties as Record; + for (const [property, setter] of PropertiesSetter.SETTERS) { + const value = properties[property]; - element.clear(); - for (const [property, setter] of PropertiesSetter.SETTERS_ENTRIES) { - const value = (this.properties as any)[property]; - - setter(value as never, element, this.properties); - } - } else { - for (const [property, setter] of PropertiesSetter.SETTERS_ENTRIES) { - const value = (this.properties as any)[property]; - - if (!Types.equals(value, oldProperties[property])) { - setter(value as never, element, this.properties); - } + if (!Types.equals(value, oldProperties[property])) { + setter(value as never, element); } } - (element as any)['properties'] = this.properties; + setValue(element, 'properties', properties); } public setProperties(properties: Properties) { @@ -499,37 +552,67 @@ class PropertiesSetter implements ShapeProperties { } public setTextDecoration(decoration: TextDecoration): ShapeProperties { - this.properties.textContent.underline = decoration === 'underline'; + const props = this.properties.textContent; + if (props) { + props.underline = decoration === 'underline'; + } return this; } public setBackgroundColor(color: RendererColor | null | undefined): ShapeProperties { - this.properties.fill = PixiHelper.toColor(getBackgroundColor(color)); + const props = this.properties.graphics; + if (props) { + props.fill = PixiHelper.toColor(getBackgroundColor(color)); + } return this; } public setForegroundColor(color: RendererColor | null | undefined): ShapeProperties { - this.properties.textStyle.fill = PixiHelper.toColor(getForegroundColor(color)); + const props = this.properties.textStyle; + if (props) { + props.fill = PixiHelper.toColor(getForegroundColor(color)); + } return this; } public setStrokeColor(color: RendererColor | null | undefined): ShapeProperties { - this.properties.stroke.color = PixiHelper.toColor(getStrokeColor(color)); + const props = this.properties.graphics; + if (props) { + props.stroke.color = PixiHelper.toColor(getStrokeColor(color)); + } return this; } public setStrokeWidth(width: number): ShapeProperties { - this.properties.stroke.width = width; + const props = this.properties.graphics; + if (props) { + props.stroke.width = width; + } + return this; + } + + public setStrokeStyle(cap: string, join: string): ShapeProperties { + const props = this.properties.graphics; + if (props) { + props.stroke.cap = cap as any; + props.stroke.join = join as any; + } return this; } public setFontSize(fontSize: TextConfig | Shape | number | null | undefined): ShapeProperties { - this.properties.textStyle.fontSize = getFontSize(fontSize); + const props = this.properties.textStyle; + if (props) { + props.fontSize = getFontSize(fontSize); + } return this; } public setFontFamily(fontFamily: TextConfig | string | null | undefined): ShapeProperties { - this.properties.textStyle.fontFamily = getFontFamily(fontFamily); + const props = this.properties.textStyle; + if (props) { + props.fontFamily = getFontFamily(fontFamily); + } return this; } @@ -542,10 +625,4 @@ class PropertiesSetter implements ShapeProperties { this.properties.textContent = { text: getText(text), markdown }; return this; } - - public setStrokeStyle(cap: string, join: string): ShapeProperties { - this.properties.stroke.cap = cap as any; - this.properties.stroke.join = join as any; - return this; - } } \ No newline at end of file diff --git a/src/wireframes/engine/shared.ts b/src/wireframes/engine/shared.ts index 5bc3aa1..9db72b8 100644 --- a/src/wireframes/engine/shared.ts +++ b/src/wireframes/engine/shared.ts @@ -135,4 +135,17 @@ export function getOpacity(value: RendererWidth | null | undefined) { function isShape(element: any): element is Shape { return Types.isFunction(element?.getAppearance); +} + +export function setValue(element: any, key: string, value: T) { + (element as any)[key] = value; +} + +export function getValue(element: any, key: string, factory?: () => T) { + let value = (element as any)[key]; + if (!value && factory) { + value = factory(); + } + + return value; } \ No newline at end of file diff --git a/src/wireframes/engine/svg/engine.ts b/src/wireframes/engine/svg/engine.ts index 72dd290..dc131d4 100644 --- a/src/wireframes/engine/svg/engine.ts +++ b/src/wireframes/engine/svg/engine.ts @@ -94,7 +94,7 @@ export class SvgEngine implements Engine { const engineEvent = new EngineMouseEvent( event, - new Vec2(x, y)); + new Vec2(Math.round(x), Math.round(y))); return engineEvent; }; @@ -122,7 +122,7 @@ export class SvgEngine implements Engine { const engineEvent = new EngineHitEvent( event, - new Vec2(x, y), + new Vec2(Math.round(x), Math.round(y)), eventObject as any, eventItem); diff --git a/src/wireframes/engine/svg/renderer.ts b/src/wireframes/engine/svg/renderer.ts index 3c88be0..93f131f 100644 --- a/src/wireframes/engine/svg/renderer.ts +++ b/src/wireframes/engine/svg/renderer.ts @@ -11,7 +11,7 @@ import * as svg from '@svgdotjs/svg.js'; import { marked } from 'marked'; import { escapeHTML, Rect2, TextMeasurer, Types } from '@app/core/utils'; import { RendererColor, RendererOpacity, RendererText, RendererWidth, ShapeProperties, ShapePropertiesFunc, ShapeRenderer, ShapeRendererFunc, TextConfig, TextDecoration } from '@app/wireframes/interface'; -import { getBackgroundColor, getFontFamily, getFontSize, getForegroundColor, getOpacity, getStrokeColor, getStrokeWidth, getText, getTextAlignment } from './../shared'; +import { getBackgroundColor, getFontFamily, getFontSize, getForegroundColor, getOpacity, getStrokeColor, getStrokeWidth, getText, getTextAlignment, getValue, setValue } from './../shared'; import { SvgHelper } from './utils'; const FACTORY_ELLIPSE = () => { @@ -31,13 +31,13 @@ const FACTORY_TEXT = () => { }; export class SvgRenderer implements ShapeRenderer { - private containerItem: svg.G = null!; - private containerIndex = 0; + private currentContainer: svg.G = null!; + private currentIndex = 0; private clipping = false; private wasClipped = false; public getContainer() { - return this.containerItem; + return this.currentContainer; } public getTextWidth(text: string, fontSize: number, fontFamily: string) { @@ -46,8 +46,8 @@ export class SvgRenderer implements ShapeRenderer { public setContainer(container: svg.G, index = 0, clipping = false) { this.clipping = clipping; - this.containerItem = container; - this.containerIndex = index; + this.currentContainer = container; + this.currentIndex = index; this.wasClipped = false; } @@ -161,12 +161,12 @@ export class SvgRenderer implements ShapeRenderer { public group(items: ShapeRendererFunc, clip?: ShapeRendererFunc, properties?: ShapePropertiesFunc) { return this.new('g', () => new svg.G(), {}, properties, group => { const clipping = this.clipping; - const container = this.containerItem; - const containerIndex = this.containerIndex; + const container = this.currentContainer; + const containerIndex = this.currentIndex; const wasClipped = this.wasClipped; - this.containerItem = group; - this.containerIndex = 0; + this.currentContainer = group; + this.currentIndex = 0; if (items) { items(this); @@ -180,8 +180,8 @@ export class SvgRenderer implements ShapeRenderer { this.cleanupAll(); this.clipping = clipping; - this.containerItem = container; - this.containerIndex = containerIndex; + this.currentContainer = container; + this.currentIndex = containerIndex; this.wasClipped = wasClipped; }); } @@ -197,26 +197,24 @@ export class SvgRenderer implements ShapeRenderer { } if (this.clipping) { - element = this.containerItem.clipper()?.get(0) as T; + element = this.currentContainer.clipper()?.get(0) as T; if (!element || element.node.tagName !== name) { element = factory(); const clipPath = new svg.ClipPath(); - clipPath.add(element); - - this.containerItem.add(clipPath); - this.containerItem.clipWith(clipPath); + this.currentContainer.add(clipPath); + this.currentContainer.clipWith(clipPath); } this.wasClipped = true; } else { - element = this.containerItem.get(this.containerIndex) as T; + element = this.currentContainer.get(this.currentIndex) as T; if (!element) { element = factory(); - element.addTo(this.containerItem); + element.addTo(this.currentContainer); } else if (element.node.tagName !== name) { const previous = element; @@ -226,7 +224,7 @@ export class SvgRenderer implements ShapeRenderer { previous.remove(); } - this.containerIndex++; + this.currentIndex++; } customInitializer?.(element); @@ -243,10 +241,10 @@ export class SvgRenderer implements ShapeRenderer { } public cleanupAll() { - const childNodes = this.containerItem.node.childNodes; + const childNodes = this.currentContainer.node.childNodes; const childrenSize = childNodes.length; - for (let i = childrenSize - 1; i >= this.containerIndex; i--) { + for (let i = childrenSize - 1; i >= this.currentIndex; i--) { const last = childNodes[i]; if (last.nodeName === 'clipPath' && this.wasClipped) { @@ -257,7 +255,7 @@ export class SvgRenderer implements ShapeRenderer { } if (!this.wasClipped) { - this.containerItem.unclip(); + this.currentContainer.unclip(); } } } @@ -295,11 +293,11 @@ const DEFAULT_PROPERTIES: Properties = { }; type Setters = { - [P in keyof T]?: (value: T[P], element: svg.Element, all: T) => void; + [P in keyof T]?: (value: T[P], element: svg.Element) => void; }; class PropertiesSetter implements ShapeProperties { - private static readonly SETTER_MAP: Setters = { + private static readonly SETTERS = Object.entries({ color: (value, element) => { SvgHelper.fastSetAttribute(element.node, 'color', value); }, @@ -397,11 +395,9 @@ class PropertiesSetter implements ShapeProperties { SvgHelper.transformByRect(element, value, false); } }, - }; + } as Setters); - public static readonly SETTERS = Object.entries(this.SETTER_MAP); public static readonly INSTANCE = new PropertiesSetter(); - private properties!: Properties; public prepare(defaults: Partial) { @@ -412,17 +408,18 @@ class PropertiesSetter implements ShapeProperties { } public apply(element: svg.Element) { - const oldProperties = (element.node as any)['properties'] || {} as any; + const oldProperties = getValue(element, 'properties', () => ({} as Record)); + const properties = this.properties as Record; for (const [property, setter] of PropertiesSetter.SETTERS) { - const value = (this.properties as any)[property]; + const value = properties[property]; if (!Types.equals(value, oldProperties[property])) { - setter(value as never, element, this.properties); + setter(value as never, element); } } - (element.node as any)['properties'] = this.properties; + setValue(element, 'properties', properties); } public setBackgroundColor(color: RendererColor | null | undefined): ShapeProperties { diff --git a/src/wireframes/engine/svg/utils.ts b/src/wireframes/engine/svg/utils.ts index 29c41b6..dfabffd 100644 --- a/src/wireframes/engine/svg/utils.ts +++ b/src/wireframes/engine/svg/utils.ts @@ -143,6 +143,8 @@ export module SvgHelper { } if (element.node.nodeName === 'ellipse') { + fastSetAttribute(element.node, 'cx', w * 0.5); + fastSetAttribute(element.node, 'cy', h * 0.5); fastSetAttribute(element.node, 'rx', w * 0.5); fastSetAttribute(element.node, 'ry', h * 0.5); } else { diff --git a/src/wireframes/renderer/TransformAdorner.tsx b/src/wireframes/renderer/TransformAdorner.tsx index cbc3476..5c21d4b 100644 --- a/src/wireframes/renderer/TransformAdorner.tsx +++ b/src/wireframes/renderer/TransformAdorner.tsx @@ -364,10 +364,11 @@ export class TransformAdorner extends React.PureComponent if (showOverlay) { this.props.overlayManager.showSnapAdorners(deltaSize); - const w = Math.floor(this.transform.size.x); - const h = Math.floor(this.transform.size.y); + const w = this.transform.size.x; + const h = this.transform.size.y; + const x = this.transform.aabb.x; - this.props.overlayManager.showInfo(this.transform, `Width: ${w}, Height: ${h}`); + this.props.overlayManager.showInfo(this.transform, `Width: ${w}, Height: ${h}, X: ${x}`); } this.debug(); diff --git a/src/wireframes/shapes/ShapeRenderer.tsx b/src/wireframes/shapes/ShapeRenderer.tsx index aa1b601..f4f1ddb 100644 --- a/src/wireframes/shapes/ShapeRenderer.tsx +++ b/src/wireframes/shapes/ShapeRenderer.tsx @@ -57,7 +57,7 @@ export const ShapeRenderer = React.memo(React.forwardRef desiredWidth / desiredHeight) { engine.doc.width(desiredWidth);