Skip to content

Commit

Permalink
feat: add angle snap, activate with shift key or settings.angleSnap =…
Browse files Browse the repository at this point in the history
… true
  • Loading branch information
robertrosman committed Jun 9, 2024
1 parent 24842e5 commit 575c825
Show file tree
Hide file tree
Showing 17 changed files with 184 additions and 33 deletions.
3 changes: 2 additions & 1 deletion src/composables/tools/useArrow/useArrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ToolSvgComponentProps, DrawEvent, Tool, BaseShape } from '@/types'
import { lineHandles } from '@/composables/tools/useMove/handles/lineHandles'
import { createShapeSvgComponent } from '@/utils/createShapeSvgComponent'
import { computed, h } from 'vue'
import { lineSnapAngles } from '@/utils/snapAngles'

export interface Arrow extends BaseShape {
type: 'arrow'
Expand Down Expand Up @@ -90,5 +91,5 @@ export function useArrow(): Tool<Arrow> {
}
}

return { type, icon, onDraw, ShapeSvgComponent, ToolSvgComponent, handles: lineHandles }
return { type, icon, onDraw, ShapeSvgComponent, ToolSvgComponent, handles: lineHandles, snapAngles: lineSnapAngles }
}
3 changes: 2 additions & 1 deletion src/composables/tools/useCrop/useCrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ToolSvgComponentProps, DrawEvent, Tool, ExportParameters, Shape, B
import { computed, h, toRef, toRefs } from 'vue'
import { rectangleHandles } from '../useMove/handles/rectangleHandles'
import { useSimplifiedHistory } from '@/composables/useSimplifiedHistory'
import { rectangleSnapAngles } from '@/utils/snapAngles'

export interface Crop extends BaseShape {
type: 'crop'
Expand Down Expand Up @@ -77,5 +78,5 @@ export function useCrop(): Tool<Crop> {
}
}

return { type, icon, onDraw, svgStyle, simplifyHistory, ToolSvgComponent, beforeExport, handles: rectangleHandles }
return { type, icon, onDraw, svgStyle, simplifyHistory, ToolSvgComponent, beforeExport, handles: rectangleHandles, snapAngles: rectangleSnapAngles }
}
3 changes: 2 additions & 1 deletion src/composables/tools/useEllipse/useEllipse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { BaseShape, DrawEvent, Tool } from '@/types'
import { ellipseHandles } from '@/composables/tools/useMove/handles/ellipseHandles'
import { createShapeSvgComponent } from '@/utils/createShapeSvgComponent'
import { h } from 'vue'
import { rectangleSnapAngles } from '@/utils/snapAngles'

export interface Ellipse extends BaseShape {
type: 'ellipse'
Expand Down Expand Up @@ -80,5 +81,5 @@ export function useEllipse({ base = 'edge'}: UseEllipseOptions = {}): Tool<Ellip
}
`

return { type, icon, onDraw, ShapeSvgComponent, svgStyle, handles: ellipseHandles }
return { type, icon, onDraw, ShapeSvgComponent, svgStyle, handles: ellipseHandles, snapAngles: rectangleSnapAngles }
}
13 changes: 11 additions & 2 deletions src/composables/tools/useKeyboardShortcuts/useKeyboardShortcuts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Tool, BaseShape } from '@/types'
import { computed } from 'vue'
import { computed, watch } from 'vue'
import { useActiveElement, useMagicKeys, whenever } from '@vueuse/core'
import { logicAnd } from '@vueuse/math'
import type { useEditor } from '@/composables/useEditor'
Expand All @@ -22,6 +22,9 @@ interface UseKeyboardShortcutsOptions {
* const tools = [useKeyboardShortcuts({ shortcuts }), useAnotherTool(), useAThirdTool]
*/
shortcuts?: Shortcuts

/** Do you want to turn angleSnap on temporarily with shift key? Defaults to true. */
angleSnapOnShift?: boolean
}

type Shortcuts = Record<string, (args: ReturnType<typeof useEditor>) => void>
Expand All @@ -45,7 +48,7 @@ export const defaultShortcuts: Shortcuts = {

const registeredKeyCodes: string[] = []

export function useKeyboardShortcuts({ shortcuts = defaultShortcuts }: UseKeyboardShortcutsOptions = {}): Tool<KeyboardShortcuts> {
export function useKeyboardShortcuts({ shortcuts = defaultShortcuts, angleSnapOnShift = true }: UseKeyboardShortcutsOptions = {}): Tool<KeyboardShortcuts> {
const type = 'keyboard-shortcuts'

const activeElement = useActiveElement()
Expand Down Expand Up @@ -73,6 +76,12 @@ export function useKeyboardShortcuts({ shortcuts = defaultShortcuts }: UseKeyboa
})
registeredKeyCodes.push(keycode)
})

if (angleSnapOnShift) {
watch(keys.shift, () => {
getActiveEditor().settings.value.angleSnap = keys.shift.value
})
}
}

return { type, onInitialize }
Expand Down
3 changes: 2 additions & 1 deletion src/composables/tools/useLine/useLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { BaseShape, DrawEvent, Tool } from '@/types'
import { lineHandles } from '@/composables/tools/useMove/handles/lineHandles'
import { createShapeSvgComponent } from '@/utils/createShapeSvgComponent'
import { h } from 'vue'
import { lineSnapAngles } from '@/utils/snapAngles'

export interface Line extends BaseShape {
type: 'line'
Expand Down Expand Up @@ -50,5 +51,5 @@ export function useLine(): Tool<Line> {
}
`

return { type, icon, onDraw, ShapeSvgComponent, svgStyle, handles: lineHandles }
return { type, icon, onDraw, ShapeSvgComponent, svgStyle, handles: lineHandles, snapAngles: lineSnapAngles }
}
6 changes: 5 additions & 1 deletion src/composables/tools/useMove/handles/ellipseHandles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Movement, Handle } from "@/types"
import type { Handle } from "@/types"

export interface EllipseLike {
x: number
Expand Down Expand Up @@ -27,6 +27,7 @@ export const ellipseHandles: Handle<EllipseLike>[] = [
},
{
name: 'top-right',
opposite: 'bottom-left',
position: ({ x, y, height, width }) => ({ x: x + width / doubleDiagonalMultiplier, y: y - height / doubleDiagonalMultiplier }),
onMove: ({ x, y }, { base }) => (base === 'center'
? { width: x * diagonalMultiplier * 2, height: -y * diagonalMultiplier * 2}
Expand All @@ -43,6 +44,7 @@ export const ellipseHandles: Handle<EllipseLike>[] = [
},
{
name: 'bottom-right',
opposite: 'top-left',
position: ({ x, y, height, width }) => ({ x: x + width / doubleDiagonalMultiplier, y: y + height / doubleDiagonalMultiplier }),
onMove: ({ x, y }, { base }) => (base === 'center'
? { width: x * diagonalMultiplier * 2, height: y * diagonalMultiplier * 2}
Expand All @@ -59,6 +61,7 @@ export const ellipseHandles: Handle<EllipseLike>[] = [
},
{
name: 'bottom-left',
opposite: 'top-right',
position: ({ x, y, height, width }) => ({ x: x - width / doubleDiagonalMultiplier, y: y + height / doubleDiagonalMultiplier }),
onMove: ({ x, y }, { base }) => (base === 'center'
? { width: -x * diagonalMultiplier * 2, height: y * diagonalMultiplier * 2}
Expand All @@ -75,6 +78,7 @@ export const ellipseHandles: Handle<EllipseLike>[] = [
},
{
name: 'top-left',
opposite: 'bottom-right',
position: ({ x, y, height, width }) => ({ x: x - width / doubleDiagonalMultiplier, y: y - height / doubleDiagonalMultiplier }),
onMove: ({ x, y }, { base }) => (base === 'center'
? { width: -x * diagonalMultiplier * 2, height: -y * diagonalMultiplier * 2}
Expand Down
6 changes: 4 additions & 2 deletions src/composables/tools/useMove/handles/lineHandles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export const lineHandles: Handle<LineLike>[] = [
{
name: 'start',
position: ({x1, y1}: LineLike) => ({x: x1, y: y1}),
onMove: ({x, y}: Movement) => ({x1: x, y1: y})
onMove: ({x, y}: Movement) => ({x1: x, y1: y}),
opposite: 'end'
},
{
name: 'base',
Expand All @@ -21,6 +22,7 @@ export const lineHandles: Handle<LineLike>[] = [
{
name: 'end',
position: ({x2, y2}: LineLike) => ({x: x2, y: y2}),
onMove: ({x, y}: Movement) => ({x2: x, y2: y})
onMove: ({x, y}: Movement) => ({x2: x, y2: y}),
opposite: 'start'
},
]
4 changes: 4 additions & 0 deletions src/composables/tools/useMove/handles/rectangleHandles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,25 @@ export const rectangleHandles: Handle<RectangleLike>[] = [
},
{
name: 'top-left',
opposite: 'bottom-right',
position: ({ x, y }: RectangleLike) => ({ x, y }),
onMove: ({ x, y }: Movement) => ({ x, y, width: -x, height: -y })
},
{
name: 'top-right',
opposite: 'bottom-left',
position: ({ x, y, width }: RectangleLike) => ({ x: x + width, y }),
onMove: ({ x, y }: Movement) => ({ width: x, height: -y, y })
},
{
name: 'bottom-left',
opposite: 'top-right',
position: ({ x, y, height }: RectangleLike) => ({ x, y: y + height }),
onMove: ({ x, y }: Movement) => ({ x, width: -x, height: y })
},
{
name: 'bottom-right',
opposite: 'top-left',
position: ({ x, y, width, height }: RectangleLike) => ({ x: x + width, y: y + height }),
onMove: ({ x, y }: Movement) => ({ width: x, height: y })
}
Expand Down
66 changes: 51 additions & 15 deletions src/composables/tools/useMove/useMove.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { useSimplifiedHistory } from '@/composables/useSimplifiedHistory'
import type { BaseShape, DrawEvent, ExportParameters, ImageHistory, Movement, Shape, SvgStyleParameters, Tool, ToolSvgComponentProps } from '@/types'
import { h, toRefs } from 'vue'
import { lineSnapAngles } from '@/utils/snapAngles'
import { snapToAngle } from '@/utils/snapToAngle'
import { h, ref, toRefs } from 'vue'

export interface Move extends BaseShape, Movement {
type: 'move'
targets: string[]
x: number
y: number
angleSnap: boolean
}

export interface UseMoveOptions {
Expand Down Expand Up @@ -40,18 +43,24 @@ export function useMove({
id,
targets: structuredClone(targets),
x: 0,
y: 0
y: 0,
angleSnap: false
}
}

function onDraw({ id, posStart, x, y }: DrawEvent): Move {
return {
function onDraw({ id, settings, posStart, x, y }: DrawEvent): Move {
const angleSnap = settings.angleSnap
settings.angleSnap = false
const move = {
type,
id,
targets: structuredClone(targets),
x: x - posStart.x,
y: y - posStart.y,
}
angleSnap
} as Move
settings.angleSnap = angleSnap
return move
}

function onDrawEnd({ activeShape }: DrawEvent): Move | undefined {
Expand All @@ -60,23 +69,50 @@ export function useMove({
: activeShape as Move
}

function applyMoveToShape(shape: BaseShape, tools: Tool<any>[], move: Move & { handle: string}) {
const tool = tools.find(tool => tool.type === shape.type)
const handles = tool?.handles
const handle = handles?.find(h => h.name === move.handle)
if (!handle || !tool) return shape

const oppositeHandle = handles?.find(h => h.name === handle.opposite)
const snapAngles = ref()
const posStart = {x: 0, y: 0}

if (move.angleSnap && handle.name === 'base') {
snapAngles.value = lineSnapAngles
}
else if (move.angleSnap && oppositeHandle) {
const position = handle.position(shape)
const oppositePosition = oppositeHandle.position(shape)
posStart.x = oppositePosition.x - position.x
posStart.y = oppositePosition.y - position.y
snapAngles.value = tool.snapAngles
}

const snapMove = snapToAngle({ snapAngles, posStart, x: move.x, y: move.y })
const diff = handle?.onMove(snapMove, shape) ?? {}
const entries = Object.entries(diff).map(([key, value]) =>
[key, shape[key as keyof typeof shape] + value]
)

return Object.assign({}, shape, Object.fromEntries(entries))
}

function simplifyHistory(history: ImageHistory<Shape[]>, tools: Tool<any>[]) {
const flatMoves = history
.filter<Move>((move): move is Move => move.type === 'move')
.flatMap(move => move.targets.map(target => {
const [handle, shapeId] = target.includes('-handle-') ? target.split('-handle-') : ['base', target]
return { shapeId, handle, x: move.x, y: move.y }
return { shapeId, handle, ...move }
}))

return history.map(shape => {
const clonedShape = {...shape}
flatMoves.filter(move => shape.id === move.shapeId)
.map(move => tools.find(tool => tool.type === shape.type)?.handles?.find(h => h.name === move.handle)?.onMove(move, shape))
.forEach(m => Object.entries(m ?? {}).forEach(([key, value]) => {
clonedShape[key as keyof typeof clonedShape] += value
}))
return clonedShape
}).filter(shape => shape.type !== 'move')
return history
.filter(shape => shape.type !== 'move')
.map(shape => flatMoves
.filter(move => shape.id === move.shapeId)
.reduce((clonedShape, move) => applyMoveToShape(clonedShape, tools, move), shape)
)
}

const ToolSvgComponent = {
Expand Down
3 changes: 2 additions & 1 deletion src/composables/tools/useRectangle/useRectangle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { BaseShape, DrawEvent, Tool } from '@/types'
import { rectangleHandles } from '@/composables/tools/useMove/handles/rectangleHandles'
import { createShapeSvgComponent } from '@/utils/createShapeSvgComponent'
import { h } from 'vue'
import { rectangleSnapAngles } from '@/utils/snapAngles'

export interface Rectangle extends BaseShape {
type: 'rectangle'
Expand Down Expand Up @@ -50,5 +51,5 @@ export function useRectangle(): Tool<Rectangle> {
}
`

return { type, icon, onDraw, ShapeSvgComponent, svgStyle, handles: rectangleHandles }
return { type, icon, onDraw, ShapeSvgComponent, svgStyle, handles: rectangleHandles, snapAngles: rectangleSnapAngles }
}
3 changes: 2 additions & 1 deletion src/composables/tools/useTextarea/useTextarea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { BaseShape, DrawEvent, ExportParameters, SvgStyleParameters, Tool }
import { rectangleHandles } from '@/composables/tools/useMove/handles/rectangleHandles'
import { createShapeSvgComponent } from '@/utils/createShapeSvgComponent'
import { h, ref } from 'vue'
import { rectangleSnapAngles } from '@/utils/snapAngles'

export interface Textarea extends BaseShape {
type: 'textarea'
Expand Down Expand Up @@ -176,5 +177,5 @@ export function useTextarea({
}
}

return { type, icon, onInitialize, onDraw, onDrawEnd, ShapeSvgComponent, svgStyle, handles: rectangleHandles, beforeExport }
return { type, icon, onInitialize, onDraw, onDrawEnd, ShapeSvgComponent, svgStyle, handles: rectangleHandles, snapAngles: rectangleSnapAngles, beforeExport }
}
21 changes: 15 additions & 6 deletions src/composables/useDraw.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { snapToAngle } from '@/utils/snapToAngle'
import { type Position, usePointer, useElementBounding, type MaybeElement } from '@vueuse/core'
import { computed, reactive, ref, watchEffect, type Ref } from 'vue'

Expand All @@ -7,7 +8,8 @@ export interface UseDrawOptions {
onDraw?: () => void
onDrawEnd?: () => void
width: number
height: number
height: number,
snapAngles?: Ref<number[] | undefined>
}

/** Without this variable you can start drawing in one editor and continue in another (with it's potentially other tool). */
Expand All @@ -19,7 +21,8 @@ export function useDraw({
onDraw,
onDrawEnd,
width,
height
height,
snapAngles
}: UseDrawOptions) {
const { x: absoluteX, y: absoluteY, pressure } = usePointer()
const {
Expand All @@ -40,10 +43,16 @@ export function useDraw({
left: 0,
top: 0
})
const x = computed(() => Math.round(((absoluteX.value - left.value) * width) / scaledWidth.value))
const y = computed(() =>
Math.round(((absoluteY.value - top.value) * height) / scaledHeight.value)
)

const snapPosition = computed(() => snapToAngle({
snapAngles,
posStart,
x: Math.round(((absoluteX.value - left.value) * width) / scaledWidth.value),
y: Math.round(((absoluteY.value - top.value) * height) / scaledHeight.value)
}))
const x = computed(() => snapPosition.value.x)
const y = computed(() => snapPosition.value.y)

const minX = computed(() => Math.max(0, Math.min(posStart.x, x.value)))
const minY = computed(() => Math.max(0, Math.min(posStart.y, y.value)))
const maxX = computed(() => Math.min(width, Math.max(posStart.x, x.value)))
Expand Down
3 changes: 3 additions & 0 deletions src/composables/useEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export function useEditor({ vpImage, tools, history, settings, width, height, em
function getActiveTool() {
return tools?.find((tool) => tool.type === (temporaryTool.value ?? settings.value.tool))
}

const snapAngles = computed(() => settings.value.angleSnap ? getActiveTool()?.snapAngles : undefined)

const drawEvent = computed<DrawEvent>(() => ({
settings: settings.value,
Expand Down Expand Up @@ -68,6 +70,7 @@ export function useEditor({ vpImage, tools, history, settings, width, height, em
target: vpImage,
width,
height,
snapAngles,
onDrawStart() {
temporaryTool.value = document.elementsFromPoint(absoluteX.value, absoluteY.value)?.[0]
?.getAttribute('class')?.split(' ')
Expand Down
Loading

0 comments on commit 575c825

Please sign in to comment.