feat: add info middleware

This commit is contained in:
chenshenhai 2024-03-23 09:53:06 +08:00
parent 591a9294ae
commit 378f9c4cbf
28 changed files with 550 additions and 111 deletions

View file

@ -6,7 +6,6 @@ import type {
ViewCalculator,
ViewCalculatorOptions,
ViewScaleInfo,
ElementSize,
ViewSizeInfo,
ViewCalculatorStorage,
ViewRectInfo,
@ -17,7 +16,6 @@ import {
is,
isViewPointInElement,
getViewPointAtElement,
isElementInView,
Store,
sortElementsViewVisiableInfoMap,
updateViewVisibleInfoMapStatus,
@ -43,7 +41,10 @@ export class Calculator implements ViewCalculator {
});
}
toGridNum(num: number): number {
toGridNum(num: number, opts?: { ignore?: boolean }): number {
if (opts?.ignore === true) {
return num;
}
// TODO
// const gridUnitSize = 1; // px;
return Math.round(num);
@ -53,10 +54,18 @@ export class Calculator implements ViewCalculator {
this.#opts = null as any;
}
isElementInView(elem: ElementSize, viewScaleInfo: ViewScaleInfo, viewSizeInfo: ViewSizeInfo): boolean {
return isElementInView(elem, { viewScaleInfo, viewSizeInfo });
needRender(elem: Element<ElementType>): boolean {
const viewVisibleInfoMap = this.#store.get('viewVisibleInfoMap');
const info = viewVisibleInfoMap[elem.uuid];
if (!info) {
return true;
}
return info.isVisibleInView;
}
/**
* @deprecated
*/
isPointInElement(p: Point, elem: Element<ElementType>, viewScaleInfo: ViewScaleInfo, viewSizeInfo: ViewSizeInfo): boolean {
const context2d = this.#opts.viewContext;
return isViewPointInElement(p, {

View file

@ -102,6 +102,8 @@ export class Viewer extends EventEmitter<BoardViewerEventMap> implements BoardVi
const { sharer } = this.#opts;
const activeStore: ActiveStore = sharer.getActiveStoreSnapshot();
const sharedStore: Record<string, any> = sharer.getSharedStoreSnapshot();
// const activeStore: ActiveStore = sharer.getActiveStoreSnapshot({ deepClone: true });
// const sharedStore: Record<string, any> = sharer.getSharedStoreSnapshot({ deepClone: true });
this.#drawFrameSnapshotQueue.push({
activeStore,
sharedStore

View file

@ -10,6 +10,7 @@ export { MiddlewareScaler, middlewareEventScale } from './middleware/scaler';
export { MiddlewareRuler, middlewareEventRuler } from './middleware/ruler';
export { MiddlewareTextEditor, middlewareEventTextEdit, middlewareEventTextChange } from './middleware/text-editor';
export { MiddlewareDragger } from './middleware/dragger';
export { MiddlewareInfo } from './middleware/info';
export class Core<E extends CoreEventMap = CoreEventMap> {
#board: Board<E>;

View file

@ -0,0 +1,130 @@
import type { PointSize, ViewContext2D } from '@idraw/types';
import { rotateByCenter } from '@idraw/util';
const fontFamily = 'monospace';
export function drawSizeInfoText(
ctx: ViewContext2D,
opts: { point: PointSize; rotateCenter: PointSize; angle: number; text: string; fontSize: number; lineHeight: number; color: string; background: string }
) {
const { point, rotateCenter, angle, text, color, background, fontSize, lineHeight } = opts;
rotateByCenter(ctx, angle, rotateCenter, () => {
ctx.$setFont({
fontWeight: '300',
fontSize,
fontFamily
});
const padding = (lineHeight - fontSize) / 2;
const textWidth = ctx.$undoPixelRatio(ctx.measureText(text).width);
const bgStart = {
x: point.x - textWidth / 2 - padding,
y: point.y
};
const bgEnd = {
x: bgStart.x + textWidth + padding * 2,
y: bgStart.y + fontSize + padding
};
const textStart = {
x: point.x - textWidth / 2,
y: point.y
};
ctx.setLineDash([]);
ctx.fillStyle = background;
ctx.beginPath();
ctx.moveTo(bgStart.x, bgStart.y);
ctx.lineTo(bgEnd.x, bgStart.y);
ctx.lineTo(bgEnd.x, bgEnd.y);
ctx.lineTo(bgStart.x, bgEnd.y);
ctx.closePath();
ctx.fill();
ctx.fillStyle = color;
ctx.textBaseline = 'top';
ctx.fillText(text, textStart.x, textStart.y + padding);
});
}
export function drawPositionInfoText(
ctx: ViewContext2D,
opts: { point: PointSize; rotateCenter: PointSize; angle: number; text: string; fontSize: number; lineHeight: number; color: string; background: string }
) {
const { point, rotateCenter, angle, text, color, background, fontSize, lineHeight } = opts;
rotateByCenter(ctx, angle, rotateCenter, () => {
ctx.$setFont({
fontWeight: '300',
fontSize,
fontFamily
});
const padding = (lineHeight - fontSize) / 2;
const textWidth = ctx.$undoPixelRatio(ctx.measureText(text).width);
const bgStart = {
x: point.x,
y: point.y
};
const bgEnd = {
x: bgStart.x + textWidth + padding * 2,
y: bgStart.y + fontSize + padding
};
const textStart = {
x: point.x + padding,
y: point.y
};
ctx.setLineDash([]);
ctx.fillStyle = background;
ctx.beginPath();
ctx.moveTo(bgStart.x, bgStart.y);
ctx.lineTo(bgEnd.x, bgStart.y);
ctx.lineTo(bgEnd.x, bgEnd.y);
ctx.lineTo(bgStart.x, bgEnd.y);
ctx.closePath();
ctx.fill();
ctx.fillStyle = color;
ctx.textBaseline = 'top';
ctx.fillText(text, textStart.x, textStart.y + padding);
});
}
export function drawAngleInfoText(
ctx: ViewContext2D,
opts: { point: PointSize; rotateCenter: PointSize; angle: number; text: string; fontSize: number; lineHeight: number; color: string; background: string }
) {
const { point, rotateCenter, angle, text, color, background, fontSize, lineHeight } = opts;
rotateByCenter(ctx, angle, rotateCenter, () => {
ctx.$setFont({
fontWeight: '300',
fontSize,
fontFamily
});
const padding = (lineHeight - fontSize) / 2;
const textWidth = ctx.$undoPixelRatio(ctx.measureText(text).width);
const bgStart = {
x: point.x,
y: point.y
};
const bgEnd = {
x: bgStart.x + textWidth + padding * 2,
y: bgStart.y + fontSize + padding
};
const textStart = {
x: point.x + padding,
y: point.y
};
ctx.setLineDash([]);
ctx.fillStyle = background;
ctx.beginPath();
ctx.moveTo(bgStart.x, bgStart.y);
ctx.lineTo(bgEnd.x, bgStart.y);
ctx.lineTo(bgEnd.x, bgEnd.y);
ctx.lineTo(bgStart.x, bgEnd.y);
ctx.closePath();
ctx.fill();
ctx.fillStyle = color;
ctx.textBaseline = 'top';
ctx.fillText(text, textStart.x, textStart.y + padding);
});
}

View file

@ -0,0 +1,140 @@
import type { BoardMiddleware, ViewRectInfo, Element } from '@idraw/types';
import { formatNumber, getViewScaleInfoFromSnapshot, getViewSizeInfoFromSnapshot, createUUID, limitAngle, rotatePoint, parseAngleToRadian } from '@idraw/util';
import { keySelectedElementList, keyActionType, keyGroupQueue } from '../selector';
import { drawSizeInfoText, drawPositionInfoText, drawAngleInfoText } from './draw-info';
import type { DeepInfoSharedStorage } from './types';
const infoBackground = '#1973bac6';
const infoTextColor = '#ffffff';
const infoFontSize = 10;
const infoLineHeight = 16;
export const MiddlewareInfo: BoardMiddleware<DeepInfoSharedStorage> = (opts) => {
const { boardContent, calculator } = opts;
const { helperContext } = boardContent;
return {
name: '@middleware/info',
beforeDrawFrame({ snapshot }) {
const { sharedStore } = snapshot;
const selectedElementList = sharedStore[keySelectedElementList];
const actionType = sharedStore[keyActionType];
const groupQueue = sharedStore[keyGroupQueue] || [];
// console.log('resizeType ===== ', resizeType);
if (selectedElementList.length === 1) {
const elem = selectedElementList[0];
if (elem && ['select', 'drag', 'resize'].includes(actionType as string)) {
const viewScaleInfo = getViewScaleInfoFromSnapshot(snapshot);
const viewSizeInfo = getViewSizeInfoFromSnapshot(snapshot);
const { x, y, w, h, angle } = elem;
const totalGroupQueue = [
...groupQueue,
...[
{
uuid: createUUID(),
x,
y,
w,
h,
angle,
type: 'group',
detail: { children: [] }
} as Element<'group'>
]
];
const calcOpts = { viewScaleInfo, viewSizeInfo };
const rangeRectInfo = calculator.calcViewRectInfoFromOrigin(elem.uuid, calcOpts);
let totalAngle = 0;
totalGroupQueue.forEach((group) => {
totalAngle += group.angle || 0;
});
const totalRadian = parseAngleToRadian(limitAngle(0 - totalAngle));
if (rangeRectInfo) {
const elemCenter = rangeRectInfo?.center;
const rectInfo: ViewRectInfo = {
topLeft: rotatePoint(elemCenter, rangeRectInfo.topLeft, totalRadian),
topRight: rotatePoint(elemCenter, rangeRectInfo.topRight, totalRadian),
bottomRight: rotatePoint(elemCenter, rangeRectInfo.bottomRight, totalRadian),
bottomLeft: rotatePoint(elemCenter, rangeRectInfo.bottomLeft, totalRadian),
center: rotatePoint(elemCenter, rangeRectInfo.center, totalRadian),
top: rotatePoint(elemCenter, rangeRectInfo.top, totalRadian),
right: rotatePoint(elemCenter, rangeRectInfo.right, totalRadian),
bottom: rotatePoint(elemCenter, rangeRectInfo.bottom, totalRadian),
left: rotatePoint(elemCenter, rangeRectInfo.left, totalRadian)
};
const x = formatNumber(elem.x, { decimalPlaces: 2 });
const y = formatNumber(elem.y, { decimalPlaces: 2 });
const w = formatNumber(elem.w, { decimalPlaces: 2 });
const h = formatNumber(elem.h, { decimalPlaces: 2 });
// // test start ----
// const ctx = helperContext;
// ctx.beginPath();
// ctx.moveTo(rectInfo.topLeft.x, rectInfo.topLeft.y);
// ctx.lineTo(rectInfo.topRight.x, rectInfo.topRight.y);
// ctx.lineTo(rectInfo.bottomRight.x, rectInfo.bottomRight.y);
// ctx.lineTo(rectInfo.bottomLeft.x, rectInfo.bottomLeft.y);
// ctx.closePath();
// ctx.strokeStyle = 'red';
// ctx.stroke();
// // test end ----
const xyText = `${formatNumber(x, { decimalPlaces: 0 })},${formatNumber(y, { decimalPlaces: 0 })}`;
const whText = `${formatNumber(w, { decimalPlaces: 0 })}x${formatNumber(h, { decimalPlaces: 0 })}`;
const angleText = `${formatNumber(elem.angle || 0, { decimalPlaces: 0 })}°`;
drawSizeInfoText(helperContext, {
point: {
x: rectInfo.bottom.x,
y: rectInfo.bottom.y + infoFontSize
},
rotateCenter: rectInfo.center,
angle: totalAngle,
text: whText,
fontSize: infoFontSize,
lineHeight: infoLineHeight,
color: infoTextColor,
background: infoBackground
});
drawPositionInfoText(helperContext, {
point: {
x: rectInfo.topLeft.x,
y: rectInfo.topLeft.y - infoFontSize * 2
},
rotateCenter: rectInfo.center,
angle: totalAngle,
text: xyText,
fontSize: infoFontSize,
lineHeight: infoLineHeight,
color: infoTextColor,
background: infoBackground
});
drawAngleInfoText(helperContext, {
point: {
x: rectInfo.top.x + infoFontSize,
y: rectInfo.top.y - infoFontSize * 2
},
rotateCenter: rectInfo.center,
angle: totalAngle,
text: angleText,
fontSize: infoFontSize,
lineHeight: infoLineHeight,
color: infoTextColor,
background: infoBackground
});
}
}
}
}
};
};

View file

@ -0,0 +1,4 @@
import { keySelectedElementList, keyActionType, keyGroupQueue } from '../selector';
import type { DeepSelectorSharedStorage } from '../selector';
export type DeepInfoSharedStorage = Pick<DeepSelectorSharedStorage, typeof keySelectedElementList | typeof keyActionType | typeof keyGroupQueue>;

View file

@ -1,11 +1,12 @@
import type { BoardMiddleware, CoreEventMap } from '@idraw/types';
import { getViewScaleInfoFromSnapshot, getViewSizeInfoFromSnapshot } from '@idraw/util';
import { drawRulerBackground, drawXRuler, drawYRuler, calcXRulerScaleList, calcYRulerScaleList, drawUnderGrid } from './util';
import { drawRulerBackground, drawXRuler, drawYRuler, calcXRulerScaleList, calcYRulerScaleList, drawUnderGrid, drawScrollerSelectedArea } from './util';
import type { DeepRulerSharedStorage } from './types';
export const middlewareEventRuler = '@middleware/show-ruler';
export const MiddlewareRuler: BoardMiddleware<Record<string, any>, CoreEventMap> = (opts) => {
const { boardContent, viewer, eventHub } = opts;
export const MiddlewareRuler: BoardMiddleware<DeepRulerSharedStorage, CoreEventMap> = (opts) => {
const { boardContent, viewer, eventHub, calculator } = opts;
const { helperContext, underContext } = boardContent;
let show: boolean = true;
let showGrid: boolean = true;
@ -35,6 +36,8 @@ export const MiddlewareRuler: BoardMiddleware<Record<string, any>, CoreEventMap>
if (show === true) {
const viewScaleInfo = getViewScaleInfoFromSnapshot(snapshot);
const viewSizeInfo = getViewSizeInfoFromSnapshot(snapshot);
drawScrollerSelectedArea(helperContext, { snapshot, calculator });
drawRulerBackground(helperContext, { viewScaleInfo, viewSizeInfo });
const xList = calcXRulerScaleList({ viewScaleInfo, viewSizeInfo });

View file

@ -0,0 +1,4 @@
import { keySelectedElementList, keyActionType } from '../selector';
import type { DeepSelectorSharedStorage } from '../selector';
export type DeepRulerSharedStorage = Pick<DeepSelectorSharedStorage, typeof keySelectedElementList | typeof keyActionType>;

View file

@ -1,5 +1,7 @@
import type { ViewScaleInfo, ViewSizeInfo, ViewContext2D } from '@idraw/types';
import { formatNumber, rotateByCenter } from '@idraw/util';
import type { Element, ViewScaleInfo, ViewSizeInfo, ViewContext2D, BoardViewerFrameSnapshot, ViewRectInfo, ViewCalculator } from '@idraw/types';
import { formatNumber, rotateByCenter, getViewScaleInfoFromSnapshot, getViewSizeInfoFromSnapshot } from '@idraw/util';
import type { DeepRulerSharedStorage } from './types';
import { keySelectedElementList, keyActionType } from '../selector';
const rulerSize = 16;
const background = '#FFFFFFA8';
@ -12,6 +14,7 @@ const fontWeight = 100;
const gridColor = '#AAAAAA20';
const gridKeyColor = '#AAAAAA40';
const lineSize = 1;
const selectedAreaColor = '#196097';
// const rulerUnit = 10;
// const rulerKeyUnit = 100;
@ -235,3 +238,62 @@ export function drawUnderGrid(
}
// TODO
}
export function drawScrollerSelectedArea(ctx: ViewContext2D, opts: { snapshot: BoardViewerFrameSnapshot<DeepRulerSharedStorage>; calculator: ViewCalculator }) {
const { snapshot, calculator } = opts;
const { sharedStore } = snapshot;
const selectedElementList = sharedStore[keySelectedElementList];
const actionType = sharedStore[keyActionType];
if (['select', 'drag', 'drag-list', 'drag-list-end'].includes(actionType as string) && selectedElementList.length > 0) {
const viewScaleInfo = getViewScaleInfoFromSnapshot(snapshot);
const viewSizeInfo = getViewSizeInfoFromSnapshot(snapshot);
const rangeRectInfoList: ViewRectInfo[] = [];
const xAreaStartList: number[] = [];
const xAreaEndList: number[] = [];
const yAreaStartList: number[] = [];
const yAreaEndList: number[] = [];
selectedElementList.forEach((elem: Element) => {
const rectInfo = calculator.calcViewRectInfoFromRange(elem.uuid, {
viewScaleInfo,
viewSizeInfo
});
if (rectInfo) {
rangeRectInfoList.push(rectInfo);
xAreaStartList.push(rectInfo.left.x);
xAreaEndList.push(rectInfo.right.x);
yAreaStartList.push(rectInfo.top.y);
yAreaEndList.push(rectInfo.bottom.y);
}
});
if (!(rangeRectInfoList.length > 0)) {
return;
}
const xAreaStart = Math.min(...xAreaStartList);
const xAreaEnd = Math.max(...xAreaEndList);
const yAreaStart = Math.min(...yAreaStartList);
const yAreaEnd = Math.max(...yAreaEndList);
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.moveTo(xAreaStart, 0);
ctx.lineTo(xAreaEnd, 0);
ctx.lineTo(xAreaEnd, rulerSize);
ctx.lineTo(xAreaStart, rulerSize);
ctx.fillStyle = selectedAreaColor;
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(0, yAreaStart);
ctx.lineTo(rulerSize, yAreaStart);
ctx.lineTo(rulerSize, yAreaEnd);
ctx.lineTo(0, yAreaEnd);
ctx.fillStyle = selectedAreaColor;
ctx.closePath();
ctx.fill();
}
}

View file

@ -2,8 +2,9 @@ import type { Point, BoardMiddleware, PointWatcherEvent, BoardWatherWheelEvent }
import { drawScroller, isPointInScrollThumb } from './util';
// import type { ScrollbarThumbType } from './util';
import { keyXThumbRect, keyYThumbRect, keyPrevPoint, keyActivePoint, keyActiveThumbType } from './config';
import type { DeepScrollerSharedStorage } from './types';
export const MiddlewareScroller: BoardMiddleware = (opts) => {
export const MiddlewareScroller: BoardMiddleware<DeepScrollerSharedStorage> = (opts) => {
const { viewer, boardContent, sharer } = opts;
const { helperContext } = boardContent;
sharer.setSharedStorage(keyXThumbRect, null); // null | ElementSize

View file

@ -0,0 +1,10 @@
import type { Point, ElementSize } from '@idraw/types';
import { keyXThumbRect, keyYThumbRect, keyPrevPoint, keyActivePoint, keyActiveThumbType } from './config';
export type DeepScrollerSharedStorage = {
[keyXThumbRect]: null | ElementSize;
[keyYThumbRect]: null | ElementSize;
[keyPrevPoint]: null | Point;
[keyActivePoint]: null | Point;
[keyActiveThumbType]: null | 'X' | 'Y';
};

View file

@ -4,7 +4,7 @@ import { keyActivePoint, keyActiveThumbType, keyPrevPoint, keyXThumbRect, keyYTh
const minScrollerWidth = 12;
const scrollerLineWidth = 16;
const scrollerThumbAlpha = 0.36;
const scrollerThumbAlpha = 0.3;
export type ScrollbarThumbType = 'X' | 'Y';

View file

@ -71,6 +71,9 @@ import {
import { calcReferenceInfo } from './reference';
import { middlewareEventTextEdit } from '../text-editor';
export { keySelectedElementList, keyActionType, keyResizeType, keyGroupQueue };
export type { DeepSelectorSharedStorage };
export const middlewareEventSelect: string = '@middleware/select';
export const middlewareEventSelectClear: string = '@middleware/select-clear';
@ -445,6 +448,7 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
sharer.setSharedStorage(keySelectedReferenceYLines, referenceInfo.yLines);
}
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
@ -479,7 +483,7 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
type: 'updateElement',
content: {
element: elem,
position: sharer.getSharedStorage(keySelectedElementPosition) || []
position: getElementPositionFromList(elem.uuid, data.elements) || []
}
},
viewSizeInfo,
@ -487,6 +491,10 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
});
}
});
// calculator.updateVisiableStatus({
// viewSizeInfo,
// viewScaleInfo
// });
sharer.setActiveStorage('data', data);
}
@ -533,22 +541,20 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
sharer
});
elems[0].angle = resizedElemSize.angle;
elems[0].angle = calculator.toGridNum(resizedElemSize.angle || 0);
} else {
const resizedElemSize = resizeElement(elems[0], { scale, start: resizeStart, end: resizeEnd, resizeType, sharer });
elems[0].x = calculator.toGridNum(resizedElemSize.x);
elems[0].y = calculator.toGridNum(resizedElemSize.y);
const calcOpts = { ignore: !!elems[0].angle };
elems[0].x = calculator.toGridNum(resizedElemSize.x, calcOpts);
elems[0].y = calculator.toGridNum(resizedElemSize.y, calcOpts);
if (elems[0].type === 'group' && elems[0].operations?.deepResize === true) {
// TODO
// elems[0].w = resizedElemSize.w;
// elems[0].h = resizedElemSize.h;
deepResizeGroupElement(elems[0] as Element<'group'>, {
w: calculator.toGridNum(resizedElemSize.w),
h: calculator.toGridNum(resizedElemSize.h)
w: calculator.toGridNum(resizedElemSize.w, calcOpts),
h: calculator.toGridNum(resizedElemSize.h, calcOpts)
});
} else {
elems[0].w = calculator.toGridNum(resizedElemSize.w);
elems[0].h = calculator.toGridNum(resizedElemSize.h);
elems[0].w = calculator.toGridNum(resizedElemSize.w, calcOpts);
elems[0].h = calculator.toGridNum(resizedElemSize.h, calcOpts);
}
}

View file

@ -14,7 +14,8 @@ export function getDefaultStorage(): IDrawStorage {
enableScroll: false,
enableSelect: false,
enableTextEdit: false,
enableDrag: false
enableDrag: false,
enableInfo: false
};
return storage;
}

View file

@ -58,11 +58,12 @@ export class iDraw {
#setFeature(feat: IDrawFeature, status: boolean) {
const store = this.#store;
if (['ruler', 'scroll', 'scale'].includes(feat)) {
if (['ruler', 'scroll', 'scale', 'info'].includes(feat)) {
const map: Record<IDrawFeature, keyof Omit<IDrawStorage, 'mode'>> = {
ruler: 'enableRuler',
scroll: 'enableScroll',
scale: 'enableScale'
scale: 'enableScale',
info: 'enableInfo'
};
store.set(map[feat], !!status);
runMiddlewares(this.#core, store);

View file

@ -1,6 +1,15 @@
import type { IDrawMode, IDrawStorage } from '@idraw/types';
import { Store } from '@idraw/util';
import { Core, MiddlewareSelector, MiddlewareScroller, MiddlewareScaler, MiddlewareRuler, MiddlewareTextEditor, MiddlewareDragger } from '@idraw/core';
import {
Core,
MiddlewareSelector,
MiddlewareScroller,
MiddlewareScaler,
MiddlewareRuler,
MiddlewareTextEditor,
MiddlewareDragger,
MiddlewareInfo
} from '@idraw/core';
import { IDrawEvent } from './event';
function isValidMode(mode: string | IDrawMode) {
@ -8,7 +17,7 @@ function isValidMode(mode: string | IDrawMode) {
}
export function runMiddlewares(core: Core<IDrawEvent>, store: Store<IDrawStorage>) {
const { enableRuler, enableScale, enableScroll, enableSelect, enableTextEdit, enableDrag } = store.getSnapshot();
const { enableRuler, enableScale, enableScroll, enableSelect, enableTextEdit, enableDrag, enableInfo } = store.getSnapshot();
if (enableScroll === true) {
core.use(MiddlewareScroller);
} else if (enableScroll === false) {
@ -44,6 +53,12 @@ export function runMiddlewares(core: Core<IDrawEvent>, store: Store<IDrawStorage
} else if (enableDrag === false) {
core.disuse(MiddlewareDragger);
}
if (enableInfo === true) {
core.use(MiddlewareInfo);
} else if (enableInfo === false) {
core.disuse(MiddlewareInfo);
}
}
export function changeMode(mode: IDrawMode, core: Core<IDrawEvent>, store: Store<IDrawStorage>) {
@ -53,6 +68,7 @@ export function changeMode(mode: IDrawMode, core: Core<IDrawEvent>, store: Store
let enableTextEdit: boolean = false;
let enableDrag: boolean = false;
let enableRuler: boolean = false;
const enableInfo: boolean = true;
let innerMode: IDrawMode = 'select';
store.set('mode', innerMode);
@ -92,6 +108,7 @@ export function changeMode(mode: IDrawMode, core: Core<IDrawEvent>, store: Store
store.set('enableTextEdit', enableTextEdit);
store.set('enableDrag', enableDrag);
store.set('enableRuler', enableRuler);
store.set('enableInfo', enableInfo);
runMiddlewares(core, store);
}

View file

@ -14,9 +14,9 @@ export function getOpacity(elem: Element): number {
export function drawBox(
ctx: ViewContext2D,
viewElem: Element<ElementType>,
viewElem: Element,
opts: {
originElem: Element<ElementType>;
originElem: Element;
calcElemSize: ElementSize;
pattern?: string | CanvasPattern | null;
renderContent: () => void;
@ -88,7 +88,7 @@ function drawClipPath(
}
}
function drawBoxBackground(
export function drawBoxBackground(
ctx: ViewContext2D,
viewElem: Element<ElementType>,
opts: { pattern?: string | CanvasPattern | null; viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }
@ -149,7 +149,7 @@ function drawBoxBackground(
}
}
function drawBoxBorder(ctx: ViewContext2D, viewElem: Element<ElementType>, opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }): void {
export function drawBoxBorder(ctx: ViewContext2D, viewElem: Element<ElementType>, opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }): void {
if (viewElem.detail.borderWidth === 0) {
return;
}

View file

@ -19,7 +19,7 @@ export function drawElementList(ctx: ViewContext2D, data: Data, opts: RendererDr
}
};
if (opts.forceDrawAll !== true) {
if (!opts.calculator?.isElementInView(elem, opts.viewScaleInfo, opts.viewSizeInfo)) {
if (!opts.calculator?.needRender(elem)) {
continue;
}
}

View file

@ -126,7 +126,7 @@ export function drawGroup(ctx: ViewContext2D, elem: Element<'group'>, opts: Rend
}
};
if (opts.forceDrawAll !== true) {
if (!calculator?.isElementInView(child, opts.viewScaleInfo, opts.viewSizeInfo)) {
if (!calculator?.needRender(child)) {
continue;
}
}

View file

@ -5,4 +5,4 @@ export { drawSVG } from './svg';
export { drawHTML } from './html';
export { drawText } from './text';
export { drawElementList } from './elements';
export { drawUnderlay } from './underlay';
export { drawLayout } from './layout';

View file

@ -0,0 +1,51 @@
import type { RendererDrawElementOptions, ViewContext2D, DataLayout, Element } from '@idraw/types';
import { calcViewElementSize, calcViewBoxSize } from '@idraw/util';
import { drawBoxShadow, drawBoxBackground, drawBoxBorder } from './box';
export function drawLayout(ctx: ViewContext2D, layout: DataLayout, opts: RendererDrawElementOptions, renderContent: (ctx: ViewContext2D) => void) {
const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
const elem: Element = { uuid: 'layout', type: 'group', x: 0, y: 0, ...layout };
const { x, y, w, h } = calcViewElementSize(elem, { viewScaleInfo, viewSizeInfo }) || elem;
const angle = 0;
const viewElem: Element = { ...elem, ...{ x, y, w, h, angle } } as Element;
ctx.globalAlpha = 1;
drawBoxShadow(ctx, viewElem, {
viewScaleInfo,
viewSizeInfo,
renderContent: () => {
drawBoxBackground(ctx, viewElem, { viewScaleInfo, viewSizeInfo });
}
});
if (layout.detail.overflow === 'hidden') {
const { viewScaleInfo, viewSizeInfo } = opts;
const elem: Element<'group'> = { uuid: 'layout', type: 'group', x: 0, y: 0, ...layout } as Element<'group'>;
const viewElemSize = calcViewElementSize(elem, { viewScaleInfo, viewSizeInfo }) || elem;
const viewElem = { ...elem, ...viewElemSize };
const { x, y, w, h, radiusList } = calcViewBoxSize(viewElem, {
viewScaleInfo,
viewSizeInfo
});
ctx.save();
ctx.fillStyle = 'transparent';
ctx.beginPath();
ctx.moveTo(x + radiusList[0], y);
ctx.arcTo(x + w, y, x + w, y + h, radiusList[1]);
ctx.arcTo(x + w, y + h, x, y + h, radiusList[2]);
ctx.arcTo(x, y + h, x, y, radiusList[3]);
ctx.arcTo(x, y, x + w, y, radiusList[0]);
ctx.closePath();
ctx.fill();
ctx.clip();
}
renderContent(ctx);
if (layout.detail.overflow === 'hidden') {
ctx.restore();
}
drawBoxBorder(ctx, viewElem, { viewScaleInfo, viewSizeInfo });
ctx.globalAlpha = parentOpacity;
}

View file

@ -1,27 +0,0 @@
import type { RendererDrawElementOptions, ViewContext2D, DataUnderlay } from '@idraw/types';
import { calcViewElementSize } from '@idraw/util';
import { drawBox, drawBoxShadow } from './box';
export function drawUnderlay(ctx: ViewContext2D, underlay: DataUnderlay, opts: RendererDrawElementOptions) {
const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
const elem = { uuid: 'underlay', ...underlay };
const { x, y, w, h } = calcViewElementSize(elem, { viewScaleInfo, viewSizeInfo }) || elem;
const angle = 0;
const viewElem = { ...elem, ...{ x, y, w, h, angle } };
drawBoxShadow(ctx, viewElem, {
viewScaleInfo,
viewSizeInfo,
renderContent: () => {
drawBox(ctx, viewElem, {
originElem: elem,
calcElemSize: { x, y, w, h, angle },
viewScaleInfo,
viewSizeInfo,
parentOpacity,
renderContent: () => {
// empty
}
});
}
});
}

View file

@ -1,6 +1,6 @@
import { EventEmitter } from '@idraw/util';
import type { LoadItemMap } from '@idraw/types';
import { drawElementList, drawUnderlay } from './draw/index';
import type { DataLayout, LoadItemMap } from '@idraw/types';
import { drawElementList, drawLayout } from './draw/index';
import { Loader } from './loader';
import type { Data, BoardRenderer, RendererOptions, RendererEventMap, RendererDrawOptions } from '@idraw/types';
@ -54,23 +54,30 @@ export class Renderer extends EventEmitter<RendererEventMap> implements BoardRen
w: opts.viewSizeInfo.width,
h: opts.viewSizeInfo.height
};
if (data.underlay) {
drawUnderlay(viewContext, data.underlay, {
loader,
calculator,
parentElementSize,
parentOpacity: 1,
...opts
});
}
drawElementList(viewContext, data, {
// if (data.underlay) {
// drawUnderlay(viewContext, data.underlay, {
// loader,
// calculator,
// parentElementSize,
// parentOpacity: 1,
// ...opts
// });
// }
const drawOpts = {
loader,
calculator,
parentElementSize,
elementAssets: data.assets,
parentOpacity: 1,
...opts
});
};
if (data.layout) {
drawLayout(viewContext, data.layout as DataLayout, drawOpts, () => {
drawElementList(viewContext, data, drawOpts);
});
} else {
drawElementList(viewContext, data, drawOpts);
}
}
scale(num: number) {

View file

@ -1,11 +1,12 @@
import type { Element, ElementType, ElementAssets } from './element';
export type DataUnderlay = Omit<Element<'rect'>, 'uuid' | 'angle'>; // TODO color background
import type { Element, ElementType, ElementAssets, ElementSize, ElementGroupDetail } from './element';
export type DataLayout = Pick<ElementSize, 'w' | 'h'> & {
detail: Omit<ElementGroupDetail, 'children'>;
};
export interface Data<E extends Record<string, any> = Record<string, any>> {
elements: Element<ElementType, E>[];
assets?: ElementAssets;
underlay?: DataUnderlay; // Rect or Color
layout?: DataLayout;
}
export type Matrix = [

View file

@ -2,7 +2,7 @@ import type { CoreOptions } from './core';
export type IDrawMode = 'select' | 'drag' | 'readOnly';
export type IDrawFeature = 'ruler' | 'scroll' | 'scale'; // TODO other feature
export type IDrawFeature = 'ruler' | 'scroll' | 'scale' | 'info'; // TODO other feature
export interface IDrawSettings {
mode?: IDrawMode;
@ -18,4 +18,5 @@ export interface IDrawStorage {
enableSelect: boolean;
enableTextEdit: boolean;
enableDrag: boolean;
enableInfo: boolean;
}

View file

@ -43,8 +43,11 @@ export interface ViewCalculatorStorage {
}
export interface ViewCalculator {
isElementInView(elem: Element<ElementType>, viewScaleInfo: ViewScaleInfo, viewSizeInfo: ViewSizeInfo): boolean;
/**
* @deprecated
*/
isPointInElement(p: Point, elem: Element<ElementType>, viewScaleInfo: ViewScaleInfo, viewSize: ViewSizeInfo): boolean;
needRender(elem: Element<ElementType>): boolean;
getPointElement(
p: Point,
opts: { data: Data; viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo; groupQueue?: Element<'group'>[] }
@ -82,7 +85,7 @@ export interface ViewCalculator {
}
): void;
toGridNum(num: number): number;
toGridNum(num: number, opts?: { ignore?: boolean }): number;
}
export type ViewRectVertexes = [PointSize, PointSize, PointSize, PointSize];

View file

@ -119,6 +119,9 @@ export function calcViewVertexes(vertexes: ViewRectVertexes, opts: { viewScaleIn
];
}
/**
* @deprecated
*/
export function isViewPointInElement(
p: Point,
opts: { context2d: ViewContext2D; element: ElementSize; viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }
@ -224,6 +227,9 @@ export function getViewPointAtElement(
return result;
}
/**
* @deprecated
*/
export function isElementInView(elem: ElementSize, opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }): boolean {
const { viewSizeInfo, viewScaleInfo } = opts;
const { width, height } = viewSizeInfo;
@ -434,7 +440,7 @@ export function calcElementViewRectInfoMap(
return {
originRectInfo,
viewRectInfo,
// viewRectInfo,
rangeRectInfo
};
}

View file

@ -18,39 +18,45 @@ export function calcViewCenterContent(data: Data, opts: { viewSizeInfo: ViewSize
let contentY: number = data?.elements?.[0]?.y || 0;
let contentW: number = data?.elements?.[0]?.w || 0;
let contentH: number = data?.elements?.[0]?.h || 0;
const { width, height } = opts.viewSizeInfo;
data.elements.forEach((elem: Element) => {
const elemSize: ElementSize = {
x: elem.x,
y: elem.y,
w: elem.w,
h: elem.h,
angle: elem.angle
};
if (elemSize.angle && (elemSize.angle > 0 || elemSize.angle < 0)) {
const ves = rotateElementVertexes(elemSize);
if (ves.length === 4) {
const xList = [ves[0].x, ves[1].x, ves[2].x, ves[3].x];
const yList = [ves[0].y, ves[1].y, ves[2].y, ves[3].y];
elemSize.x = Math.min(...xList);
elemSize.y = Math.min(...yList);
elemSize.w = Math.abs(Math.max(...xList) - Math.min(...xList));
elemSize.h = Math.abs(Math.max(...yList) - Math.min(...yList));
if (data.layout && data.layout?.detail?.overflow === 'hidden') {
contentX = 0;
contentY = 0;
contentW = data.layout.w || 0;
contentH = data.layout.h || 0;
} else {
data.elements.forEach((elem: Element) => {
const elemSize: ElementSize = {
x: elem.x,
y: elem.y,
w: elem.w,
h: elem.h,
angle: elem.angle
};
if (elemSize.angle && (elemSize.angle > 0 || elemSize.angle < 0)) {
const ves = rotateElementVertexes(elemSize);
if (ves.length === 4) {
const xList = [ves[0].x, ves[1].x, ves[2].x, ves[3].x];
const yList = [ves[0].y, ves[1].y, ves[2].y, ves[3].y];
elemSize.x = Math.min(...xList);
elemSize.y = Math.min(...yList);
elemSize.w = Math.abs(Math.max(...xList) - Math.min(...xList));
elemSize.h = Math.abs(Math.max(...yList) - Math.min(...yList));
}
}
}
const areaStartX = Math.min(elemSize.x, contentX);
const areaStartY = Math.min(elemSize.y, contentY);
const areaStartX = Math.min(elemSize.x, contentX);
const areaStartY = Math.min(elemSize.y, contentY);
const areaEndX = Math.max(elemSize.x + elemSize.w, contentX + contentW);
const areaEndY = Math.max(elemSize.y + elemSize.h, contentY + contentH);
const areaEndX = Math.max(elemSize.x + elemSize.w, contentX + contentW);
const areaEndY = Math.max(elemSize.y + elemSize.h, contentY + contentH);
contentX = areaStartX;
contentY = areaStartY;
contentW = Math.abs(areaEndX - areaStartX);
contentH = Math.abs(areaEndY - areaStartY);
});
contentX = areaStartX;
contentY = areaStartY;
contentW = Math.abs(areaEndX - areaStartX);
contentH = Math.abs(areaEndY - areaStartY);
});
}
if (contentW > 0 && contentH > 0) {
const scaleW = formatNumber(width / contentW, { decimalPlaces: 4 });