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
@@ -0,0 +1,294 @@
import * as React from 'react';
import { Utils, SVGContainer, TLBounds } from '@tldraw/core';
import {
TriangleShape,
TDShapeType,
TDMeta,
TDShape,
DashStyle,
BINDING_DISTANCE,
GHOSTED_OPACITY,
LABEL_POINT,
} from '@toeverything/components/board-types';
import { TDShapeUtil } from '../TDShapeUtil';
import {
defaultStyle,
getBoundsRectangle,
transformRectangle,
transformSingleRectangle,
getFontStyle,
TextLabel,
getShapeStyle,
} from '../shared';
import {
intersectBoundsPolygon,
intersectLineSegmentPolyline,
intersectRayLineSegment,
} from '@tldraw/intersect';
import Vec from '@tldraw/vec';
import { getTriangleCentroid, getTrianglePoints } from './triangle-helpers';
import { styled } from '@toeverything/components/ui';
import { DrawTriangle } from './components/DrawTriangle';
import { DashedTriangle } from './components/DashedTriangle';
import { TriangleBindingIndicator } from './components/TriangleBindingIndicator';
type T = TriangleShape;
type E = HTMLDivElement;
export class TriangleUtil extends TDShapeUtil<T, E> {
type = TDShapeType.Triangle as const;
override canBind = true;
override canClone = true;
override canEdit = true;
getShape = (props: Partial<T>): T => {
return Utils.deepMerge<T>(
{
id: 'id',
type: TDShapeType.Triangle,
name: 'Triangle',
parentId: 'page',
childIndex: 1,
point: [0, 0],
size: [1, 1],
rotation: 0,
style: defaultStyle,
label: '',
labelPoint: [0.5, 0.5],
workspace: props.workspace,
},
props
);
};
Component = TDShapeUtil.Component<T, E, TDMeta>(
(
{
shape,
bounds,
isBinding,
isEditing,
isSelected,
isGhost,
meta,
events,
onShapeChange,
onShapeBlur,
},
ref
) => {
const {
id,
label = '',
size,
style,
labelPoint = LABEL_POINT,
} = shape;
const font = getFontStyle(style);
const styles = getShapeStyle(style, meta.isDarkMode);
const Component =
style.dash === DashStyle.Draw ? DrawTriangle : DashedTriangle;
const handleLabelChange = React.useCallback(
(label: string) => onShapeChange?.({ id, label }),
[onShapeChange]
);
const offsetY = React.useMemo(() => {
const center = Vec.div(size, 2);
const centroid = getTriangleCentroid(size);
return (centroid[1] - center[1]) * 0.72;
}, [size]);
return (
<FullWrapper ref={ref} {...events}>
<TextLabel
font={font}
text={label}
color={styles.stroke}
offsetX={(labelPoint[0] - 0.5) * bounds.width}
offsetY={
offsetY + (labelPoint[1] - 0.5) * bounds.height
}
isEditing={isEditing}
onChange={handleLabelChange}
onBlur={onShapeBlur}
/>
<SVGContainer
id={shape.id + '_svg'}
opacity={isGhost ? GHOSTED_OPACITY : 1}
>
{isBinding && <TriangleBindingIndicator size={size} />}
<Component
id={id}
style={style}
size={size}
isSelected={isSelected}
isDarkMode={meta.isDarkMode}
/>
</SVGContainer>
</FullWrapper>
);
}
);
Indicator = TDShapeUtil.Indicator<T>(({ shape }) => {
const { size } = shape;
return <polygon points={getTrianglePoints(size).join()} />;
});
private get_points(shape: T) {
const {
rotation = 0,
point: [x, y],
size: [w, h],
} = shape;
return [
[x + w / 2, y],
[x, y + h],
[x + w, y + h],
].map(pt => Vec.rotWith(pt, this.getCenter(shape), rotation));
}
override shouldRender = (prev: T, next: T) => {
return (
next.size !== prev.size ||
next.style !== prev.style ||
next.label !== prev.label
);
};
getBounds = (shape: T) => {
return getBoundsRectangle(shape, this.boundsCache);
};
override getExpandedBounds = (shape: T) => {
return Utils.getBoundsFromPoints(
getTrianglePoints(shape.size, this.bindingDistance).map(pt =>
Vec.add(pt, shape.point)
)
);
};
override hitTestLineSegment = (
shape: T,
A: number[],
B: number[]
): boolean => {
return intersectLineSegmentPolyline(A, B, this.get_points(shape))
.didIntersect;
};
override hitTestBounds = (shape: T, bounds: TLBounds): boolean => {
return (
Utils.boundsContained(this.getBounds(shape), bounds) ||
intersectBoundsPolygon(bounds, this.get_points(shape)).length > 0
);
};
override getBindingPoint = <K extends TDShape>(
shape: T,
fromShape: K,
point: number[],
origin: number[],
direction: number[],
bindAnywhere: boolean
) => {
// Algorithm time! We need to find the binding point (a normalized point inside of the shape, or around the shape, where the arrow will point to) and the distance from the binding shape to the anchor.
const expandedBounds = this.getExpandedBounds(shape);
if (!Utils.pointInBounds(point, expandedBounds)) return;
const points = getTrianglePoints(shape.size).map(pt =>
Vec.add(pt, shape.point)
);
const expandedPoints = getTrianglePoints(
shape.size,
this.bindingDistance
).map(pt => Vec.add(pt, shape.point));
const closestDistanceToEdge = Utils.pointsToLineSegments(points, true)
.map(([a, b]) => Vec.distanceToLineSegment(a, b, point))
.sort((a, b) => a - b)[0];
if (
!(
Utils.pointInPolygon(point, expandedPoints) ||
closestDistanceToEdge < this.bindingDistance
)
)
return;
const intersections = Utils.pointsToLineSegments(
expandedPoints.concat([expandedPoints[0]])
)
.map(segment =>
intersectRayLineSegment(
origin,
direction,
segment[0],
segment[1]
)
)
.filter(intersection => intersection.didIntersect)
.flatMap(intersection => intersection.points);
if (!intersections.length) return;
// The center of the triangle
const center = Vec.add(getTriangleCentroid(shape.size), shape.point);
// Find furthest intersection between ray from origin through point and expanded bounds. TODO: What if the shape has a curve? In that case, should we intersect the circle-from-three-points instead?
const intersection = intersections.sort(
(a, b) => Vec.dist(b, origin) - Vec.dist(a, origin)
)[0];
// The point between the handle and the intersection
const middlePoint = Vec.med(point, intersection);
let anchor: number[];
let distance: number;
if (bindAnywhere) {
anchor =
Vec.dist(point, center) < BINDING_DISTANCE / 2 ? center : point;
distance = 0;
} else {
if (
Vec.distanceToLineSegment(point, middlePoint, center) <
BINDING_DISTANCE / 2
) {
anchor = center;
} else {
anchor = middlePoint;
}
if (Utils.pointInPolygon(point, points)) {
distance = this.bindingDistance;
} else {
distance = Math.max(
this.bindingDistance,
closestDistanceToEdge
);
}
}
const bindingPoint = Vec.divV(
Vec.sub(anchor, [expandedBounds.minX, expandedBounds.minY]),
[expandedBounds.width, expandedBounds.height]
);
return {
point: Vec.clampV(bindingPoint, 0, 1),
distance,
};
};
override transform = transformRectangle;
override transformSingle = transformSingleRectangle;
}
const FullWrapper = styled('div')({ width: '100%', height: '100%' });
@@ -0,0 +1,68 @@
import * as React from 'react';
import { Utils } from '@tldraw/core';
import type { ShapeStyles } from '@toeverything/components/board-types';
import { getShapeStyle } from '../../shared';
import { getTrianglePoints } from '../triangle-helpers';
import Vec from '@tldraw/vec';
interface TriangleSvgProps {
id: string;
size: number[];
style: ShapeStyles;
isSelected: boolean;
isDarkMode: boolean;
}
export const DashedTriangle = React.memo(function DashedTriangle({
id,
size,
style,
isSelected,
isDarkMode,
}: TriangleSvgProps) {
const { stroke, strokeWidth, fill } = getShapeStyle(style, isDarkMode);
const sw = 1 + strokeWidth * 1.618;
const points = getTrianglePoints(size);
const sides = Utils.pointsToLineSegments(points, true);
const paths = sides.map(([start, end], i) => {
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
Vec.dist(start, end),
strokeWidth * 1.618,
style.dash
);
return (
<line
key={id + '_' + i}
x1={start[0]}
y1={start[1]}
x2={end[0]}
y2={end[1]}
stroke={stroke}
strokeWidth={sw}
strokeLinecap="round"
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
/>
);
});
const bgPath = points.join();
return (
<>
<polygon
className={
style.isFilled || isSelected
? 'tl-fill-hitarea'
: 'tl-stroke-hitarea'
}
points={bgPath}
/>
{style.isFilled && (
<polygon fill={fill} points={bgPath} pointerEvents="none" />
)}
<g pointerEvents="stroke">{paths}</g>
</>
);
});
@@ -0,0 +1,49 @@
import * as React from 'react';
import { getShapeStyle } from '../../shared';
import type { ShapeStyles } from '@toeverything/components/board-types';
import {
getTriangleIndicatorPathTDSnapshot,
getTrianglePath,
} from '../triangle-helpers';
interface TriangleSvgProps {
id: string;
size: number[];
style: ShapeStyles;
isSelected: boolean;
isDarkMode: boolean;
}
export const DrawTriangle = React.memo(function DrawTriangle({
id,
size,
style,
isSelected,
isDarkMode,
}: TriangleSvgProps) {
const { stroke, strokeWidth, fill } = getShapeStyle(style, isDarkMode);
const path_td_snapshot = getTrianglePath(id, size, style);
const indicatorPath = getTriangleIndicatorPathTDSnapshot(id, size, style);
return (
<>
<path
className={
style.isFilled || isSelected
? 'tl-fill-hitarea'
: 'tl-stroke-hitarea'
}
d={indicatorPath}
/>
{style.isFilled && (
<path d={indicatorPath} fill={fill} pointerEvents="none" />
)}
<path
d={path_td_snapshot}
fill={stroke}
stroke={stroke}
strokeWidth={strokeWidth}
pointerEvents="none"
/>
</>
);
});
@@ -0,0 +1,20 @@
import * as React from 'react';
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
import { getTrianglePoints } from '../triangle-helpers';
interface TriangleBindingIndicatorProps {
size: number[];
}
export function TriangleBindingIndicator({
size,
}: TriangleBindingIndicatorProps) {
const trianglePoints = getTrianglePoints(size).join();
return (
<polygon
className="tl-binding-indicator"
points={trianglePoints}
strokeWidth={BINDING_DISTANCE * 2}
/>
);
}
@@ -0,0 +1 @@
export * from './TriangleUtil';
@@ -0,0 +1,113 @@
import { Utils } from '@tldraw/core';
import Vec from '@tldraw/vec';
import getStroke, { getStrokePoints } from 'perfect-freehand';
import type { ShapeStyles } from '@toeverything/components/board-types';
import { getShapeStyle, getOffsetPolygon } from '../shared';
export function getTrianglePoints(size: number[], offset = 0, rotation = 0) {
const [w, h] = size;
let points = [
[w / 2, 0],
[w, h],
[0, h],
];
if (offset) points = getOffsetPolygon(points, offset);
if (rotation)
points = points.map(pt => Vec.rotWith(pt, [w / 2, h / 2], rotation));
return points;
}
export function getTriangleCentroid(size: number[]) {
const [w, h] = size;
const points = [
[w / 2, 0],
[w, h],
[0, h],
];
return [
(points[0][0] + points[1][0] + points[2][0]) / 3,
(points[0][1] + points[1][1] + points[2][1]) / 3,
];
}
function getTriangleDrawPoints(
id: string,
size: number[],
strokeWidth: number
) {
const [w, h] = size;
const getRandom = Utils.rng(id);
// Random corner offsets
const offsets = Array.from(Array(3)).map(() => {
return [
getRandom() * strokeWidth * 0.75,
getRandom() * strokeWidth * 0.75,
];
});
// Corners
const corners = [
Vec.add([w / 2, 0], offsets[0]),
Vec.add([w, h], offsets[1]),
Vec.add([0, h], offsets[2]),
];
// Which side to start drawing first
const rm = Math.round(Math.abs(getRandom() * 2 * 3));
// Number of points per side
// Inset each line by the corner radii and let the freehand algo
// interpolate points for the corners.
const lines = Utils.rotateArray(
[
Vec.pointsBetween(corners[0], corners[1], 32),
Vec.pointsBetween(corners[1], corners[2], 32),
Vec.pointsBetween(corners[2], corners[0], 32),
],
rm
);
// For the final points, include the first half of the first line again,
// so that the line wraps around and avoids ending on a sharp corner.
// This has a bit of finesse and magic—if you change the points between
// function, then you'll likely need to change this one too.
const points = [...lines.flat(), ...lines[0]];
return {
points,
};
}
function getDrawStrokeInfo(id: string, size: number[], style: ShapeStyles) {
const { strokeWidth } = getShapeStyle(style);
const { points } = getTriangleDrawPoints(id, size, strokeWidth);
const options = {
size: strokeWidth,
thinning: 0.65,
streamline: 0.3,
smoothing: 1,
simulatePressure: false,
last: true,
};
return { points, options };
}
export function getTrianglePath(
id: string,
size: number[],
style: ShapeStyles
) {
const { points, options } = getDrawStrokeInfo(id, size, style);
const stroke = getStroke(points, options);
return Utils.getSvgPathFromStroke(stroke);
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export function getTriangleIndicatorPathTDSnapshot(
id: string,
size: number[],
style: ShapeStyles
) {
const { points, options } = getDrawStrokeInfo(id, size, style);
const strokePoints = getStrokePoints(points, options);
return Utils.getSvgPathFromStroke(
strokePoints.map(pt => pt.point.slice(0, 2)),
false
);
}