init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions

View File

@@ -0,0 +1,363 @@
import * as React from 'react';
import { Utils, SVGContainer, TLBounds } from '@tldraw/core';
import { Vec } from '@tldraw/vec';
import {
defaultStyle,
getShapeStyle,
getFontStyle,
TextLabel,
} from '../shared';
import {
EllipseShape,
DashStyle,
TDShapeType,
TDShape,
TransformInfo,
TDMeta,
GHOSTED_OPACITY,
LABEL_POINT,
} from '@toeverything/components/board-types';
import { TDShapeUtil } from '../TDShapeUtil';
import {
intersectEllipseBounds,
intersectLineSegmentEllipse,
intersectRayEllipse,
} from '@tldraw/intersect';
import { getEllipseIndicatorPath } from './ellipse-helpers';
import { DrawEllipse } from './components/DrawEllipse';
import { DashedEllipse } from './components/DashedEllipse';
import { styled } from '@toeverything/components/ui';
type T = EllipseShape;
type E = HTMLDivElement;
type M = TDMeta;
export class EllipseUtil extends TDShapeUtil<T, E> {
type = TDShapeType.Ellipse as const;
override canBind = true;
override canClone = true;
override canEdit = true;
getShape = (props: Partial<T>): T => {
return Utils.deepMerge<T>(
{
id: 'id',
type: TDShapeType.Ellipse,
name: 'Ellipse',
parentId: 'page',
childIndex: 1,
point: [0, 0],
radius: [1, 1],
rotation: 0,
style: defaultStyle,
label: '',
labelPoint: [0.5, 0.5],
workspace: props.workspace,
},
props
);
};
Component = TDShapeUtil.Component<T, E, TDMeta>(
(
{
shape,
isGhost,
isSelected,
isBinding,
isEditing,
meta,
bounds,
events,
onShapeChange,
onShapeBlur,
},
ref
) => {
const {
id,
radius,
style,
label = '',
labelPoint = LABEL_POINT,
} = shape;
const font = getFontStyle(shape.style);
const styles = getShapeStyle(style, meta.isDarkMode);
const strokeWidth = styles.strokeWidth;
const sw = 1 + strokeWidth * 1.618;
const rx = Math.max(0, radius[0] - sw / 2);
const ry = Math.max(0, radius[1] - sw / 2);
const Component =
style.dash === DashStyle.Draw ? DrawEllipse : DashedEllipse;
const handleLabelChange = React.useCallback(
(label: string) => onShapeChange?.({ id, label }),
[onShapeChange]
);
return (
<FullWrapper ref={ref} {...events}>
<TextLabel
isEditing={isEditing}
onChange={handleLabelChange}
onBlur={onShapeBlur}
font={font}
text={label}
color={styles.stroke}
offsetX={(labelPoint[0] - 0.5) * bounds.width}
offsetY={(labelPoint[1] - 0.5) * bounds.height}
/>
<SVGContainer
id={shape.id + '_svg'}
opacity={isGhost ? GHOSTED_OPACITY : 1}
>
{isBinding && (
<ellipse
className="tl-binding-indicator"
cx={radius[0]}
cy={radius[1]}
rx={rx}
ry={ry}
strokeWidth={this.bindingDistance}
/>
)}
<Component
id={id}
radius={radius}
style={style}
isSelected={isSelected}
isDarkMode={meta.isDarkMode}
/>
</SVGContainer>
</FullWrapper>
);
}
);
Indicator = TDShapeUtil.Indicator<T, M>(({ shape }) => {
const { id, radius, style } = shape;
const styles = getShapeStyle(style);
const strokeWidth = styles.strokeWidth;
const sw = 1 + strokeWidth * 1.618;
const rx = Math.max(0, radius[0] - sw / 2);
const ry = Math.max(0, radius[1] - sw / 2);
return style.dash === DashStyle.Draw ? (
<path d={getEllipseIndicatorPath(id, radius, style)} />
) : (
<ellipse cx={radius[0]} cy={radius[1]} rx={rx} ry={ry} />
);
});
override hitTestPoint = (shape: T, point: number[]): boolean => {
return (
Utils.pointInBounds(point, this.getRotatedBounds(shape)) &&
Utils.pointInEllipse(
point,
this.getCenter(shape),
shape.radius[0],
shape.radius[1],
shape.rotation || 0
)
);
};
override hitTestLineSegment = (
shape: T,
A: number[],
B: number[]
): boolean => {
return intersectLineSegmentEllipse(
A,
B,
this.getCenter(shape),
shape.radius[0],
shape.radius[1],
shape.rotation || 0
).didIntersect;
};
getBounds = (shape: T) => {
return Utils.getFromCache(this.boundsCache, shape, () => {
return Utils.getRotatedEllipseBounds(
shape.point[0],
shape.point[1],
shape.radius[0],
shape.radius[1],
0
);
});
};
override getRotatedBounds = (shape: T): TLBounds => {
return Utils.getRotatedEllipseBounds(
shape.point[0],
shape.point[1],
shape.radius[0],
shape.radius[1],
shape.rotation
);
};
override hitTestBounds = (shape: T, bounds: TLBounds): boolean => {
const shapeBounds = this.getBounds(shape);
return (
Utils.boundsContained(shapeBounds, bounds) ||
intersectEllipseBounds(
this.getCenter(shape),
shape.radius[0],
shape.radius[1],
shape.rotation || 0,
bounds
).length > 0
);
};
override shouldRender = (prev: T, next: T): boolean => {
return (
next.radius !== prev.radius ||
next.style !== prev.style ||
next.label !== prev.label
);
};
override getCenter = (shape: T): number[] => {
return Vec.add(shape.point, shape.radius);
};
override getBindingPoint = <K extends TDShape>(
shape: T,
fromShape: K,
point: number[],
origin: number[],
direction: number[],
bindAnywhere: boolean
) => {
const expandedBounds = this.getExpandedBounds(shape);
const center = this.getCenter(shape);
let bindingPoint: number[];
let distance: number;
if (
!Utils.pointInEllipse(
point,
center,
shape.radius[0] + this.bindingDistance,
shape.radius[1] + this.bindingDistance
)
) {
return;
}
if (bindAnywhere) {
if (Vec.dist(point, this.getCenter(shape)) < 12) {
bindingPoint = [0.5, 0.5];
} else {
bindingPoint = Vec.divV(
Vec.sub(point, [expandedBounds.minX, expandedBounds.minY]),
[expandedBounds.width, expandedBounds.height]
);
}
distance = 0;
} else {
let intersection = intersectRayEllipse(
origin,
direction,
center,
shape.radius[0],
shape.radius[1],
shape.rotation || 0
).points.sort(
(a, b) => Vec.dist(a, origin) - Vec.dist(b, origin)
)[0];
if (!intersection) {
intersection = intersectLineSegmentEllipse(
point,
center,
center,
shape.radius[0],
shape.radius[1],
shape.rotation || 0
).points.sort(
(a, b) => Vec.dist(a, point) - Vec.dist(b, point)
)[0];
}
if (!intersection) {
return undefined;
}
// The anchor is a point between the handle and the intersection
const anchor = Vec.med(point, intersection);
if (
Vec.distanceToLineSegment(
point,
anchor,
this.getCenter(shape)
) < 12
) {
// If we're close to the center, snap to the center
bindingPoint = [0.5, 0.5];
} else {
// Or else calculate a normalized point
bindingPoint = Vec.divV(
Vec.sub(anchor, [expandedBounds.minX, expandedBounds.minY]),
[expandedBounds.width, expandedBounds.height]
);
}
if (
Utils.pointInEllipse(
point,
center,
shape.radius[0],
shape.radius[1],
shape.rotation || 0
)
) {
// Pad the arrow out by 16 points
distance = this.bindingDistance / 2;
} else {
// Find the distance between the point and the ellipse
const innerIntersection = intersectLineSegmentEllipse(
point,
center,
center,
shape.radius[0],
shape.radius[1],
shape.rotation || 0
).points[0];
if (!innerIntersection) return undefined;
distance = Math.max(
this.bindingDistance / 2,
Vec.dist(point, innerIntersection)
);
}
}
return {
point: bindingPoint,
distance,
};
};
override transform = (
shape: T,
bounds: TLBounds,
{ scaleX, scaleY, initialShape }: TransformInfo<T>
): Partial<T> => {
const { rotation = 0 } = initialShape;
return {
point: [bounds.minX, bounds.minY],
radius: [bounds.width / 2, bounds.height / 2],
rotation:
(scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0)
? -(rotation || 0)
: rotation || 0,
};
};
override transformSingle = (shape: T, bounds: TLBounds): Partial<T> => {
return {
point: Vec.toFixed([bounds.minX, bounds.minY]),
radius: Vec.div([bounds.width, bounds.height], 2),
};
};
}
const FullWrapper = styled('div')({ width: '100%', height: '100%' });

View File

@@ -0,0 +1,60 @@
import * as React from 'react';
import { Utils } from '@tldraw/core';
import type { ShapeStyles } from '@toeverything/components/board-types';
import { getShapeStyle } from '../../shared';
interface EllipseSvgProps {
radius: number[];
style: ShapeStyles;
isSelected: boolean;
isDarkMode: boolean;
}
export const DashedEllipse = React.memo(function DashedEllipse({
radius,
style,
isSelected,
isDarkMode,
}: EllipseSvgProps) {
const { stroke, strokeWidth, fill } = getShapeStyle(style, isDarkMode);
const sw = 1 + strokeWidth * 1.618;
const rx = Math.max(0, radius[0] - sw / 2);
const ry = Math.max(0, radius[1] - sw / 2);
const perimeter = Utils.perimeterOfEllipse(rx, ry);
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
perimeter < 64 ? perimeter * 2 : perimeter,
strokeWidth * 1.618,
style.dash,
4
);
return (
<>
<ellipse
className={
style.isFilled || isSelected
? 'tl-fill-hitarea'
: 'tl-stroke-hitarea'
}
cx={radius[0]}
cy={radius[1]}
rx={radius[0]}
ry={radius[1]}
/>
<ellipse
cx={radius[0]}
cy={radius[1]}
rx={rx}
ry={ry}
fill={fill}
stroke={stroke}
strokeWidth={sw}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
pointerEvents="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</>
);
});

View File

@@ -0,0 +1,56 @@
import * as React from 'react';
import { getShapeStyle } from '../../shared';
import type { ShapeStyles } from '@toeverything/components/board-types';
import { getEllipseIndicatorPath, getEllipsePath } from '../ellipse-helpers';
interface EllipseSvgProps {
id: string;
radius: number[];
style: ShapeStyles;
isSelected: boolean;
isDarkMode: boolean;
}
export const DrawEllipse = React.memo(function DrawEllipse({
id,
radius,
style,
isSelected,
isDarkMode,
}: EllipseSvgProps) {
const { stroke, strokeWidth, fill } = getShapeStyle(style, isDarkMode);
const innerPath = getEllipsePath(id, radius, style);
return (
<>
<ellipse
className={
style.isFilled || isSelected
? 'tl-fill-hitarea'
: 'tl-stroke-hitarea'
}
cx={radius[0]}
cy={radius[1]}
rx={radius[0]}
ry={radius[1]}
/>
{style.isFilled && (
<path
d={getEllipseIndicatorPath(id, radius, style)}
stroke="none"
fill={fill}
pointerEvents="none"
/>
)}
<path
d={innerPath}
fill={stroke}
stroke={stroke}
strokeWidth={strokeWidth}
pointerEvents="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</>
);
});

View File

@@ -0,0 +1,75 @@
import { Utils } from '@tldraw/core';
import { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand';
import { EASINGS } from '@toeverything/components/board-types';
import type { ShapeStyles } from '@toeverything/components/board-types';
import { getShapeStyle } from '../shared/shape-styles';
export function getEllipseStrokePoints(
id: string,
radius: number[],
style: ShapeStyles
) {
const { strokeWidth } = getShapeStyle(style);
const getRandom = Utils.rng(id);
const rx = radius[0] + getRandom() * strokeWidth * 2;
const ry = radius[1] + getRandom() * strokeWidth * 2;
const perimeter = Utils.perimeterOfEllipse(rx, ry);
const points: number[][] = [];
const start = Math.PI + Math.PI * getRandom();
const extra = Math.abs(getRandom());
const count = Math.max(16, perimeter / 10);
for (let i = 0; i < count; i++) {
const t = EASINGS.easeInOutSine(i / (count + 1));
const rads = start * 2 + Math.PI * (2 + extra) * t;
const c = Math.cos(rads);
const s = Math.sin(rads);
points.push([
rx * c + radius[0],
ry * s + radius[1],
t + 0.5 + getRandom() / 2,
]);
}
return getStrokePoints(points, {
size: 1 + strokeWidth * 2,
thinning: 0.618,
end: { taper: perimeter / 8 },
start: { taper: perimeter / 12 },
streamline: 0,
simulatePressure: true,
});
}
export function getEllipsePath(
id: string,
radius: number[],
style: ShapeStyles
) {
const { strokeWidth } = getShapeStyle(style);
const getRandom = Utils.rng(id);
const rx = radius[0] + getRandom() * strokeWidth * 2;
const ry = radius[1] + getRandom() * strokeWidth * 2;
const perimeter = Utils.perimeterOfEllipse(rx, ry);
return Utils.getSvgPathFromStroke(
getStrokeOutlinePoints(getEllipseStrokePoints(id, radius, style), {
size: 2 + strokeWidth * 2,
thinning: 0.618,
end: { taper: perimeter / 8 },
start: { taper: perimeter / 12 },
streamline: 0,
simulatePressure: true,
})
);
}
export function getEllipseIndicatorPath(
id: string,
radius: number[],
style: ShapeStyles
) {
return Utils.getSvgPathFromStroke(
getEllipseStrokePoints(id, radius, style).map(pt =>
pt.point.slice(0, 2)
),
false
);
}

View File

@@ -0,0 +1 @@
export * from './EllipseUtil';