kopia lustrzana https://github.com/Tldraw/Tldraw
Improves outlines
rodzic
b746601de7
commit
addf4185f0
|
@ -1,5 +1,5 @@
|
|||
import { useSelector } from "state"
|
||||
import { CircleShape, ShapeProps } from "types"
|
||||
import { Indicator, HoverIndicator } from "./indicator"
|
||||
import ShapeGroup from "./shape-g"
|
||||
|
||||
function BaseCircle({
|
||||
|
@ -9,25 +9,25 @@ function BaseCircle({
|
|||
strokeWidth = 0,
|
||||
}: ShapeProps<CircleShape>) {
|
||||
return (
|
||||
<circle
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={radius}
|
||||
fill={fill}
|
||||
stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
<>
|
||||
<HoverIndicator as="circle" cx={radius} cy={radius} r={radius - 1} />
|
||||
<circle
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={radius - strokeWidth / 2}
|
||||
fill={fill}
|
||||
stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
<Indicator as="circle" cx={radius} cy={radius} r={radius - 1} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Circle({ id, point, radius }: CircleShape) {
|
||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||
return (
|
||||
<ShapeGroup id={id} point={point}>
|
||||
<BaseCircle radius={radius} />
|
||||
{isSelected && (
|
||||
<BaseCircle radius={radius} fill="none" stroke="blue" strokeWidth={1} />
|
||||
)}
|
||||
</ShapeGroup>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,40 +1,39 @@
|
|||
import { useSelector } from "state"
|
||||
import { Indicator, HoverIndicator } from "./indicator"
|
||||
import { DotShape, ShapeProps } from "types"
|
||||
import ShapeGroup from "./shape-g"
|
||||
|
||||
const dotRadius = 4
|
||||
|
||||
function BaseDot({
|
||||
fill = "#999",
|
||||
stroke = "none",
|
||||
strokeWidth = 0,
|
||||
strokeWidth = 1,
|
||||
}: ShapeProps<DotShape>) {
|
||||
return (
|
||||
<>
|
||||
<circle
|
||||
cx={strokeWidth}
|
||||
cy={strokeWidth}
|
||||
r={8}
|
||||
fill="transparent"
|
||||
stroke="none"
|
||||
strokeWidth="0"
|
||||
<HoverIndicator
|
||||
as="circle"
|
||||
cx={dotRadius}
|
||||
cy={dotRadius}
|
||||
r={dotRadius - 1}
|
||||
/>
|
||||
<circle
|
||||
cx={strokeWidth}
|
||||
cy={strokeWidth}
|
||||
r={Math.max(1, 4 - strokeWidth)}
|
||||
cx={dotRadius}
|
||||
cy={dotRadius}
|
||||
r={dotRadius - strokeWidth / 2}
|
||||
fill={fill}
|
||||
stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
<Indicator as="circle" cx={dotRadius} cy={dotRadius} r={dotRadius - 1} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Dot({ id, point }: DotShape) {
|
||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||
return (
|
||||
<ShapeGroup id={id} point={point}>
|
||||
<BaseDot />
|
||||
{isSelected && <BaseDot fill="none" stroke="blue" strokeWidth={1} />}
|
||||
</ShapeGroup>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import styled from "styles"
|
||||
|
||||
const Indicator = styled("path", {
|
||||
fill: "none",
|
||||
stroke: "transparent",
|
||||
strokeWidth: "2",
|
||||
pointerEvents: "none",
|
||||
strokeLineCap: "round",
|
||||
strokeLinejoin: "round",
|
||||
})
|
||||
|
||||
const HoverIndicator = styled("path", {
|
||||
fill: "none",
|
||||
stroke: "transparent",
|
||||
strokeWidth: "8",
|
||||
pointerEvents: "all",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
})
|
||||
|
||||
export { Indicator, HoverIndicator }
|
|
@ -1,5 +1,5 @@
|
|||
import { useSelector } from "state"
|
||||
import { PolylineShape, ShapeProps } from "types"
|
||||
import { Indicator, HoverIndicator } from "./indicator"
|
||||
import ShapeGroup from "./shape-g"
|
||||
|
||||
function BasePolyline({
|
||||
|
@ -10,28 +10,24 @@ function BasePolyline({
|
|||
}: ShapeProps<PolylineShape>) {
|
||||
return (
|
||||
<>
|
||||
<polyline
|
||||
points={points.toString()}
|
||||
fill="none"
|
||||
stroke="transparent"
|
||||
strokeWidth={12}
|
||||
/>
|
||||
<HoverIndicator as="polyline" points={points.toString()} />
|
||||
<polyline
|
||||
points={points.toString()}
|
||||
fill={fill}
|
||||
stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<Indicator as="polyline" points={points.toString()} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Polyline({ id, point, points }: PolylineShape) {
|
||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||
return (
|
||||
<ShapeGroup id={id} point={point}>
|
||||
<BasePolyline points={points} />
|
||||
{isSelected && <BasePolyline points={points} fill="none" stroke="blue" />}
|
||||
</ShapeGroup>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useSelector } from "state"
|
||||
import { RectangleShape, ShapeProps } from "types"
|
||||
import { HoverIndicator, Indicator } from "./indicator"
|
||||
import ShapeGroup from "./shape-g"
|
||||
|
||||
function BaseRectangle({
|
||||
|
@ -9,26 +9,38 @@ function BaseRectangle({
|
|||
strokeWidth = 0,
|
||||
}: ShapeProps<RectangleShape>) {
|
||||
return (
|
||||
<rect
|
||||
x={strokeWidth}
|
||||
y={strokeWidth}
|
||||
width={size[0] - strokeWidth * 2}
|
||||
height={size[1] - strokeWidth * 2}
|
||||
fill={fill}
|
||||
stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
<>
|
||||
<HoverIndicator
|
||||
as="rect"
|
||||
x={1}
|
||||
y={1}
|
||||
width={size[0] - 2}
|
||||
height={size[1] - 2}
|
||||
/>
|
||||
<rect
|
||||
x={strokeWidth / 2}
|
||||
y={strokeWidth / 2}
|
||||
width={size[0] - strokeWidth}
|
||||
height={size[1] - strokeWidth}
|
||||
fill={fill}
|
||||
stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
<Indicator
|
||||
as="rect"
|
||||
x={1}
|
||||
y={1}
|
||||
width={size[0] - 2}
|
||||
height={size[1] - 2}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Rectangle({ id, point, size }: RectangleShape) {
|
||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||
return (
|
||||
<ShapeGroup id={id} point={point}>
|
||||
<BaseRectangle size={size} />
|
||||
{isSelected && (
|
||||
<BaseRectangle size={size} fill="none" stroke="blue" strokeWidth={1} />
|
||||
)}
|
||||
</ShapeGroup>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import state from "state"
|
||||
import state, { useSelector } from "state"
|
||||
import React, { useCallback, useRef } from "react"
|
||||
import { getPointerEventInfo } from "utils/utils"
|
||||
import { Indicator, HoverIndicator } from "./indicator"
|
||||
import styled from "styles"
|
||||
|
||||
export default function ShapeGroup({
|
||||
id,
|
||||
|
@ -12,6 +14,7 @@ export default function ShapeGroup({
|
|||
point: number[]
|
||||
}) {
|
||||
const rGroup = useRef<SVGGElement>(null)
|
||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
|
@ -44,8 +47,9 @@ export default function ShapeGroup({
|
|||
)
|
||||
|
||||
return (
|
||||
<g
|
||||
<StyledGroup
|
||||
ref={rGroup}
|
||||
isSelected={isSelected}
|
||||
transform={`translate(${point})`}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerUp={handlePointerUp}
|
||||
|
@ -53,6 +57,31 @@ export default function ShapeGroup({
|
|||
onPointerLeave={handlePointerLeave}
|
||||
>
|
||||
{children}
|
||||
</g>
|
||||
</StyledGroup>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledGroup = styled("g", {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: "0",
|
||||
},
|
||||
variants: {
|
||||
isSelected: {
|
||||
true: {
|
||||
[`& ${Indicator}`]: {
|
||||
stroke: "$selected",
|
||||
},
|
||||
[`&:hover ${HoverIndicator}`]: {
|
||||
opacity: "1",
|
||||
stroke: "$hint",
|
||||
},
|
||||
},
|
||||
false: {
|
||||
[`&:hover ${HoverIndicator}`]: {
|
||||
opacity: "1",
|
||||
stroke: "$hint",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import useKeyboardEvents from "hooks/useKeyboardEvents"
|
||||
import Canvas from "./canvas/canvas"
|
||||
import StatusBar from "./status-bar"
|
||||
|
||||
export default function Editor() {
|
||||
useKeyboardEvents()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Canvas />
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { useEffect } from "react"
|
||||
import state from "state"
|
||||
import { getKeyboardEventInfo } from "utils/utils"
|
||||
|
||||
export default function useKeyboardEvents() {
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
state.send("CANCELLED")
|
||||
}
|
||||
|
||||
state.send("PRESSED_KEY", getKeyboardEventInfo(e))
|
||||
}
|
||||
|
||||
function handleKeyUp(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
state.send("CANCELLED")
|
||||
}
|
||||
|
||||
state.send("RELEASED_KEY", getKeyboardEventInfo(e))
|
||||
}
|
||||
|
||||
document.body.addEventListener("keydown", handleKeyDown)
|
||||
document.body.addEventListener("keyup", handleKeyUp)
|
||||
return () => {
|
||||
document.body.removeEventListener("keydown", handleKeyDown)
|
||||
document.body.removeEventListener("keyup", handleKeyUp)
|
||||
}
|
||||
}, [])
|
||||
}
|
|
@ -10,8 +10,8 @@ import {
|
|||
} from "utils/intersections"
|
||||
|
||||
interface BrushSnapshot {
|
||||
selectedIds: string[]
|
||||
shapes: { shape: Shape; test: (bounds: Bounds) => boolean }[]
|
||||
selectedIds: Set<string>
|
||||
shapes: { id: string; test: (bounds: Bounds) => boolean }[]
|
||||
}
|
||||
|
||||
export default class BrushSession extends BaseSession {
|
||||
|
@ -31,25 +31,33 @@ export default class BrushSession extends BaseSession {
|
|||
|
||||
const brushBounds = getBoundsFromPoints(origin, point)
|
||||
|
||||
data.selectedIds = [
|
||||
...snapshot.selectedIds,
|
||||
...snapshot.shapes
|
||||
.filter(({ test }) => test(brushBounds))
|
||||
.map(({ shape }) => shape.id),
|
||||
]
|
||||
for (let { test, id } of snapshot.shapes) {
|
||||
if (test(brushBounds)) {
|
||||
data.selectedIds.add(id)
|
||||
} else if (data.selectedIds.has(id)) {
|
||||
data.selectedIds.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
data.brush = brushBounds
|
||||
}
|
||||
|
||||
cancel = (data: Data) => {
|
||||
data.brush = undefined
|
||||
data.selectedIds = this.snapshot.selectedIds
|
||||
data.selectedIds = new Set(this.snapshot.selectedIds)
|
||||
}
|
||||
|
||||
complete = (data: Data) => {
|
||||
data.brush = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a snapshot of the current selected ids, for each shape that is
|
||||
* not already selected, the shape's id and a test to see whether the
|
||||
* brush will intersect that shape. For tests, start broad -> fine.
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
static getSnapshot(data: Data): BrushSnapshot {
|
||||
const {
|
||||
selectedIds,
|
||||
|
@ -57,19 +65,17 @@ export default class BrushSession extends BaseSession {
|
|||
currentPageId,
|
||||
} = current(data)
|
||||
|
||||
const currentlySelected = new Set(selectedIds)
|
||||
|
||||
return {
|
||||
selectedIds: [...data.selectedIds],
|
||||
selectedIds: new Set(data.selectedIds),
|
||||
shapes: Object.values(pages[currentPageId].shapes)
|
||||
.filter((shape) => !currentlySelected.has(shape.id))
|
||||
.filter((shape) => !selectedIds.has(shape.id))
|
||||
.map((shape) => {
|
||||
switch (shape.type) {
|
||||
case ShapeType.Dot: {
|
||||
const bounds = shapeUtils[shape.type].getBounds(shape)
|
||||
|
||||
return {
|
||||
shape,
|
||||
id: shape.id,
|
||||
test: (brushBounds: Bounds) =>
|
||||
boundsContained(bounds, brushBounds) ||
|
||||
intersectCircleBounds(shape.point, 4, brushBounds).length > 0,
|
||||
|
@ -79,7 +85,7 @@ export default class BrushSession extends BaseSession {
|
|||
const bounds = shapeUtils[shape.type].getBounds(shape)
|
||||
|
||||
return {
|
||||
shape,
|
||||
id: shape.id,
|
||||
test: (brushBounds: Bounds) =>
|
||||
boundsContained(bounds, brushBounds) ||
|
||||
intersectCircleBounds(
|
||||
|
@ -93,7 +99,7 @@ export default class BrushSession extends BaseSession {
|
|||
const bounds = shapeUtils[shape.type].getBounds(shape)
|
||||
|
||||
return {
|
||||
shape,
|
||||
id: shape.id,
|
||||
test: (brushBounds: Bounds) =>
|
||||
boundsContained(bounds, brushBounds) ||
|
||||
boundsCollide(bounds, brushBounds),
|
||||
|
@ -106,10 +112,11 @@ export default class BrushSession extends BaseSession {
|
|||
)
|
||||
|
||||
return {
|
||||
shape,
|
||||
id: shape.id,
|
||||
test: (brushBounds: Bounds) =>
|
||||
boundsContained(bounds, brushBounds) ||
|
||||
intersectPolylineBounds(points, brushBounds).length > 0,
|
||||
(boundsCollide(bounds, brushBounds) &&
|
||||
intersectPolylineBounds(points, brushBounds).length > 0),
|
||||
}
|
||||
}
|
||||
default: {
|
||||
|
|
|
@ -12,7 +12,7 @@ const initialData: Data = {
|
|||
},
|
||||
brush: undefined,
|
||||
pointedId: null,
|
||||
selectedIds: [],
|
||||
selectedIds: new Set([]),
|
||||
currentPageId: "page0",
|
||||
document: defaultDocument,
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ const state = createState({
|
|||
},
|
||||
conditions: {
|
||||
isPointedShapeSelected(data) {
|
||||
return data.selectedIds.includes(data.pointedId)
|
||||
return data.selectedIds.has(data.pointedId)
|
||||
},
|
||||
isPressingShiftKey(data, payload: { shiftKey: boolean }) {
|
||||
return payload.shiftKey
|
||||
|
@ -93,14 +93,14 @@ const state = createState({
|
|||
data.pointedId = undefined
|
||||
},
|
||||
clearSelectedIds(data) {
|
||||
data.selectedIds = []
|
||||
data.selectedIds.clear()
|
||||
},
|
||||
pullPointedIdFromSelectedIds(data) {
|
||||
const { selectedIds, pointedId } = data
|
||||
selectedIds.splice(selectedIds.indexOf(pointedId, 1))
|
||||
selectedIds.delete(pointedId)
|
||||
},
|
||||
pushPointedIdToSelectedIds(data) {
|
||||
data.selectedIds.push(data.pointedId)
|
||||
data.selectedIds.add(data.pointedId)
|
||||
},
|
||||
// Camera
|
||||
zoomCamera(data, payload: { delta: number; point: number[] }) {
|
||||
|
|
|
@ -8,6 +8,8 @@ const { styled, global, css, theme, getCssString } = createCss({
|
|||
colors: {
|
||||
brushFill: "rgba(0,0,0,.1)",
|
||||
brushStroke: "rgba(0,0,0,.5)",
|
||||
hint: "rgba(66, 133, 244, 0.200)",
|
||||
selected: "rgba(66, 133, 244, 1.000)",
|
||||
},
|
||||
space: {},
|
||||
fontSizes: {
|
||||
|
|
6
types.ts
6
types.ts
|
@ -5,7 +5,7 @@ export interface Data {
|
|||
}
|
||||
brush?: Bounds
|
||||
currentPageId: string
|
||||
selectedIds: string[]
|
||||
selectedIds: Set<string>
|
||||
pointedId?: string
|
||||
document: {
|
||||
pages: Record<string, Page>
|
||||
|
@ -121,4 +121,6 @@ export type ShapeSpecificProps<T extends Shape> = Pick<
|
|||
>
|
||||
|
||||
export type ShapeProps<T extends Shape> = Partial<BaseShapeStyles> &
|
||||
ShapeSpecificProps<T>
|
||||
ShapeSpecificProps<T> & { id?: Shape["id"] }
|
||||
|
||||
export type ShapeIndicatorProps<T extends Shape> = ShapeSpecificProps<T>
|
||||
|
|
|
@ -848,3 +848,8 @@ export function getPointerEventInfo(e: React.PointerEvent | WheelEvent) {
|
|||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||
return { point: [e.clientX, e.clientY], shiftKey, ctrlKey, metaKey, altKey }
|
||||
}
|
||||
|
||||
export function getKeyboardEventInfo(e: React.KeyboardEvent | KeyboardEvent) {
|
||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||
return { key: e.key, shiftKey, ctrlKey, metaKey, altKey }
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue