Adds select all, starts on rotation

This commit is contained in:
Steve Ruiz 2021-05-17 22:27:18 +01:00
parent abd310aa2e
commit e2aac4b267
33 changed files with 608 additions and 255 deletions

View file

@ -6,6 +6,12 @@ import styled from "styles"
export default function BoundsBg() { export default function BoundsBg() {
const rBounds = useRef<SVGRectElement>(null) const rBounds = useRef<SVGRectElement>(null)
const bounds = useSelector((state) => state.values.selectedBounds) const bounds = useSelector((state) => state.values.selectedBounds)
const singleSelection = useSelector((s) => {
if (s.data.selectedIds.size === 1) {
const selected = Array.from(s.data.selectedIds.values())[0]
return s.data.document.pages[s.data.currentPageId].shapes[selected]
}
})
if (!bounds) return null if (!bounds) return null
@ -24,6 +30,12 @@ export default function BoundsBg() {
rBounds.current.setPointerCapture(e.pointerId) rBounds.current.setPointerCapture(e.pointerId)
state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds")) state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
}} }}
transform={
singleSelection &&
`rotate(${singleSelection.rotation * (180 / Math.PI)},${
minX + width / 2
}, ${minY + width / 2})`
}
/> />
) )
} }

View file

@ -8,6 +8,12 @@ import { lerp } from "utils/utils"
export default function Bounds() { export default function Bounds() {
const zoom = useSelector((state) => state.data.camera.zoom) const zoom = useSelector((state) => state.data.camera.zoom)
const bounds = useSelector((state) => state.values.selectedBounds) const bounds = useSelector((state) => state.values.selectedBounds)
const singleSelection = useSelector((s) => {
if (s.data.selectedIds.size === 1) {
const selected = Array.from(s.data.selectedIds.values())[0]
return s.data.document.pages[s.data.currentPageId].shapes[selected]
}
})
const isBrushing = useSelector((state) => state.isIn("brushSelecting")) const isBrushing = useSelector((state) => state.isIn("brushSelecting"))
if (!bounds) return null if (!bounds) return null
@ -18,7 +24,15 @@ export default function Bounds() {
const cp = p * 2 const cp = p * 2
return ( return (
<g pointerEvents={isBrushing ? "none" : "all"}> <g
pointerEvents={isBrushing ? "none" : "all"}
transform={
singleSelection &&
`rotate(${singleSelection.rotation * (180 / Math.PI)},${
minX + width / 2
}, ${minY + width / 2})`
}
>
<StyledBounds <StyledBounds
x={minX} x={minX}
y={minY} y={minY}
@ -82,10 +96,35 @@ export default function Bounds() {
height={cp} height={cp}
corner={TransformCorner.BottomLeft} corner={TransformCorner.BottomLeft}
/> />
<RotateHandle x={minX + width / 2} y={minY - cp * 2} r={cp / 2} />
</g> </g>
) )
} }
function RotateHandle({ x, y, r }: { x: number; y: number; r: number }) {
const rRotateHandle = useRef<SVGCircleElement>(null)
return (
<StyledRotateHandle
ref={rRotateHandle}
cx={x}
cy={y}
r={r}
onPointerDown={(e) => {
e.stopPropagation()
rRotateHandle.current.setPointerCapture(e.pointerId)
state.send("POINTED_ROTATE_HANDLE", inputs.pointerDown(e, "rotate"))
}}
onPointerUp={(e) => {
e.stopPropagation()
rRotateHandle.current.releasePointerCapture(e.pointerId)
rRotateHandle.current.replaceWith(rRotateHandle.current)
state.send("STOPPED_POINTING", inputs.pointerDown(e, "rotate"))
}}
/>
)
}
function Corner({ function Corner({
x, x,
y, y,
@ -99,32 +138,10 @@ function Corner({
height: number height: number
corner: TransformCorner corner: TransformCorner
}) { }) {
const rRotateCorner = useRef<SVGRectElement>(null)
const rCorner = useRef<SVGRectElement>(null) const rCorner = useRef<SVGRectElement>(null)
const isTop = corner.includes("top")
const isLeft = corner.includes("bottom")
return ( return (
<g> <g>
<StyledRotateCorner
ref={rRotateCorner}
x={x + width * (isLeft ? -1.25 : -0.5)}
y={y + width * (isTop ? -1.25 : -0.5)}
width={width * 1.75}
height={height * 1.75}
onPointerDown={(e) => {
e.stopPropagation()
rRotateCorner.current.setPointerCapture(e.pointerId)
state.send("POINTED_ROTATE_CORNER", inputs.pointerDown(e, corner))
}}
onPointerUp={(e) => {
e.stopPropagation()
rRotateCorner.current.releasePointerCapture(e.pointerId)
rRotateCorner.current.replaceWith(rRotateCorner.current)
state.send("STOPPED_POINTING", inputs.pointerDown(e, corner))
}}
/>
<StyledCorner <StyledCorner
ref={rCorner} ref={rCorner}
x={x + width * -0.5} x={x + width * -0.5}
@ -252,13 +269,15 @@ const StyledCorner = styled("rect", {
}, },
}) })
const StyledRotateHandle = styled("circle", {
stroke: "$bounds",
fill: "#fff",
zStrokeWidth: 2,
cursor: "grab",
})
const StyledBounds = styled("rect", { const StyledBounds = styled("rect", {
fill: "none", fill: "none",
stroke: "$bounds", stroke: "$bounds",
zStrokeWidth: 2, zStrokeWidth: 2,
}) })
const StyledRotateCorner = styled("rect", {
cursor: "grab",
fill: "transparent",
})

View file

@ -63,7 +63,9 @@ function Shape({ id }: { id: string }) {
ref={rGroup} ref={rGroup}
isHovered={isHovered} isHovered={isHovered}
isSelected={isSelected} isSelected={isSelected}
transform={`translate(${shape.point})`} transform={`rotate(${shape.rotation * (180 / Math.PI)},${getShapeUtils(
shape
).getCenter(shape)}) translate(${shape.point})`}
onPointerDown={handlePointerDown} onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp} onPointerUp={handlePointerUp}
onPointerEnter={handlePointerEnter} onPointerEnter={handlePointerEnter}

View file

@ -37,7 +37,7 @@ export default function CodePanel() {
const file = useSelector( const file = useSelector(
(s) => s.data.document.code[s.data.currentCodeFileId] (s) => s.data.document.code[s.data.currentCodeFileId]
) )
const isOpen = true const isOpen = useSelector((s) => s.data.settings.isCodeOpen)
const fontSize = useSelector((s) => s.data.settings.fontSize) const fontSize = useSelector((s) => s.data.settings.fontSize)
const local = useStateDesigner({ const local = useStateDesigner({
@ -115,11 +115,11 @@ export default function CodePanel() {
const { error } = local.data const { error } = local.data
return ( return (
<Panel.Root data-bp-desktop ref={rContainer} isCollapsed={!isOpen}> <Panel.Root data-bp-desktop ref={rContainer} isOpen={isOpen}>
{isOpen ? ( {isOpen ? (
<Panel.Layout> <Panel.Layout>
<Panel.Header> <Panel.Header>
<IconButton onClick={() => state.send("CLOSED_CODE_PANEL")}> <IconButton onClick={() => state.send("TOGGLED_CODE_PANEL_OPEN")}>
<X /> <X />
</IconButton> </IconButton>
<h3>Code</h3> <h3>Code</h3>
@ -169,7 +169,7 @@ export default function CodePanel() {
</Panel.Footer> </Panel.Footer>
</Panel.Layout> </Panel.Layout>
) : ( ) : (
<IconButton onClick={() => state.send("OPENED_CODE_PANEL")}> <IconButton onClick={() => state.send("TOGGLED_CODE_PANEL_OPEN")}>
<Code /> <Code />
</IconButton> </IconButton>
)} )}

View file

@ -14,10 +14,10 @@ export default function ControlPanel() {
(state) => Object.keys(state.data.codeControls), (state) => Object.keys(state.data.codeControls),
deepCompareArrays deepCompareArrays
) )
const isOpen = true const isOpen = useSelector((s) => Object.keys(s.data.codeControls).length > 0)
return ( return (
<Panel.Root data-bp-desktop ref={rContainer} isCollapsed={!isOpen}> <Panel.Root data-bp-desktop ref={rContainer} isOpen={isOpen}>
{isOpen ? ( {isOpen ? (
<Panel.Layout> <Panel.Layout>
<Panel.Header> <Panel.Header>

View file

@ -12,9 +12,12 @@ export const Root = styled("div", {
boxShadow: "0px 2px 25px rgba(0,0,0,.16)", boxShadow: "0px 2px 25px rgba(0,0,0,.16)",
variants: { variants: {
isCollapsed: { isOpen: {
true: {}, true: {},
false: {}, false: {
height: 34,
width: 34,
},
}, },
}, },
}) })

View file

@ -19,6 +19,15 @@ export default function useKeyboardEvents() {
state.send("DELETED", getKeyboardEventInfo(e)) state.send("DELETED", getKeyboardEventInfo(e))
} }
if (e.key === "s" && metaKey(e)) {
e.preventDefault()
state.send("SAVED")
}
if (e.key === "a" && metaKey(e)) {
e.preventDefault()
state.send("SELECTED_ALL")
}
if (e.key === "v" && !(metaKey(e) || e.shiftKey || e.altKey)) { if (e.key === "v" && !(metaKey(e) || e.shiftKey || e.altKey)) {
state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e)) state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e))
} }

View file

@ -4,5 +4,8 @@ import state from "state"
export default function useLoadOnMount() { export default function useLoadOnMount() {
useEffect(() => { useEffect(() => {
state.send("MOUNTED") state.send("MOUNTED")
return () => {
state.send("UNMOUNTED")
}
}, []) }, [])
} }

View file

@ -3,7 +3,7 @@ import state, { useSelector } from "state"
export default function useTheme() { export default function useTheme() {
const theme = useSelector((state) => const theme = useSelector((state) =>
state.data.settings.darkMode ? "dark" : "light" state.data.settings.isDarkMode ? "dark" : "light"
) )
const toggleTheme = useCallback(() => state.send("TOGGLED_THEME"), []) const toggleTheme = useCallback(() => state.send("TOGGLED_THEME"), [])

View file

@ -18,7 +18,7 @@ export default class Circle extends CodeShape<CircleShape> {
rotation: 0, rotation: 0,
radius: 20, radius: 20,
style: { style: {
fill: "#777", fill: "rgba(142, 143, 142, 1.000)",
stroke: "#000", stroke: "#000",
strokeWidth: 1, strokeWidth: 1,
}, },

View file

@ -17,7 +17,7 @@ export default class Dot extends CodeShape<DotShape> {
point: [0, 0], point: [0, 0],
rotation: 0, rotation: 0,
style: { style: {
fill: "#777", fill: "rgba(142, 143, 142, 1.000)",
stroke: "#000", stroke: "#000",
strokeWidth: 1, strokeWidth: 1,
}, },

View file

@ -19,7 +19,7 @@ export default class Ellipse extends CodeShape<EllipseShape> {
radiusY: 20, radiusY: 20,
rotation: 0, rotation: 0,
style: { style: {
fill: "#777", fill: "rgba(142, 143, 142, 1.000)",
stroke: "#000", stroke: "#000",
strokeWidth: 1, strokeWidth: 1,
}, },

View file

@ -19,7 +19,7 @@ export default class Line extends CodeShape<LineShape> {
direction: [-0.5, 0.5], direction: [-0.5, 0.5],
rotation: 0, rotation: 0,
style: { style: {
fill: "#777", fill: "rgba(142, 143, 142, 1.000)",
stroke: "#000", stroke: "#000",
strokeWidth: 1, strokeWidth: 1,
}, },

View file

@ -19,7 +19,7 @@ export default class Ray extends CodeShape<RayShape> {
direction: [0, 1], direction: [0, 1],
rotation: 0, rotation: 0,
style: { style: {
fill: "#777", fill: "rgba(142, 143, 142, 1.000)",
stroke: "#000", stroke: "#000",
strokeWidth: 1, strokeWidth: 1,
}, },

View file

@ -19,7 +19,7 @@ export default class Rectangle extends CodeShape<RectangleShape> {
size: [100, 100], size: [100, 100],
rotation: 0, rotation: 0,
style: { style: {
fill: "#777", fill: "rgba(142, 143, 142, 1.000)",
stroke: "#000", stroke: "#000",
strokeWidth: 1, strokeWidth: 1,
}, },

View file

@ -21,7 +21,7 @@ const circle = createShape<CircleShape>({
rotation: 0, rotation: 0,
radius: 20, radius: 20,
style: { style: {
fill: "#777", fill: "rgba(142, 143, 142, 1.000)",
stroke: "#000", stroke: "#000",
}, },
...props, ...props,
@ -56,6 +56,10 @@ const circle = createShape<CircleShape>({
return bounds return bounds
}, },
getCenter(shape) {
return [shape.point[0] + shape.radius, shape.point[1] + shape.radius]
},
hitTest(shape, point) { hitTest(shape, point) {
return pointInCircle( return pointInCircle(
point, point,

View file

@ -21,7 +21,7 @@ const dot = createShape<DotShape>({
point: [0, 0], point: [0, 0],
rotation: 0, rotation: 0,
style: { style: {
fill: "#777", fill: "rgba(142, 143, 142, 1.000)",
stroke: "#000", stroke: "#000",
}, },
...props, ...props,
@ -55,6 +55,10 @@ const dot = createShape<DotShape>({
return bounds return bounds
}, },
getCenter(shape) {
return shape.point
},
hitTest(shape, test) { hitTest(shape, test) {
return true return true
}, },

View file

@ -22,7 +22,7 @@ const ellipse = createShape<EllipseShape>({
radiusY: 20, radiusY: 20,
rotation: 0, rotation: 0,
style: { style: {
fill: "#777", fill: "rgba(142, 143, 142, 1.000)",
stroke: "#000", stroke: "#000",
}, },
...props, ...props,
@ -60,6 +60,10 @@ const ellipse = createShape<EllipseShape>({
return bounds return bounds
}, },
getCenter(shape) {
return [shape.point[0] + shape.radiusX, shape.point[1] + shape.radiusY]
},
hitTest(shape, point) { hitTest(shape, point) {
return pointInEllipse( return pointInEllipse(
point, point,

View file

@ -36,6 +36,9 @@ export interface ShapeUtility<K extends Shape> {
// Get the bounds of the a shape. // Get the bounds of the a shape.
getBounds(this: ShapeUtility<K>, shape: K): Bounds getBounds(this: ShapeUtility<K>, shape: K): Bounds
// Get the center of the shape
getCenter(this: ShapeUtility<K>, shape: K): number[]
// Test whether a point lies within a shape. // Test whether a point lies within a shape.
hitTest(this: ShapeUtility<K>, shape: K, test: number[]): boolean hitTest(this: ShapeUtility<K>, shape: K, test: number[]): boolean

View file

@ -21,7 +21,7 @@ const line = createShape<LineShape>({
direction: [0, 0], direction: [0, 0],
rotation: 0, rotation: 0,
style: { style: {
fill: "#777", fill: "rgba(142, 143, 142, 1.000)",
stroke: "#000", stroke: "#000",
}, },
...props, ...props,
@ -63,6 +63,10 @@ const line = createShape<LineShape>({
return bounds return bounds
}, },
getCenter(shape) {
return shape.point
},
hitTest(shape, test) { hitTest(shape, test) {
return true return true
}, },

View file

@ -58,6 +58,11 @@ const polyline = createShape<PolylineShape>({
return bounds return bounds
}, },
getCenter(shape) {
const bounds = this.getBounds(shape)
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
},
hitTest(shape, point) { hitTest(shape, point) {
let pt = vec.sub(point, shape.point) let pt = vec.sub(point, shape.point)
let prev = shape.points[0] let prev = shape.points[0]

View file

@ -4,7 +4,6 @@ import { RayShape, ShapeType } from "types"
import { createShape } from "./index" import { createShape } from "./index"
import { boundsContained } from "utils/bounds" import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections" import { intersectCircleBounds } from "utils/intersections"
import styled from "styles"
import { DotCircle } from "components/canvas/misc" import { DotCircle } from "components/canvas/misc"
const ray = createShape<RayShape>({ const ray = createShape<RayShape>({
@ -22,7 +21,7 @@ const ray = createShape<RayShape>({
direction: [0, 1], direction: [0, 1],
rotation: 0, rotation: 0,
style: { style: {
fill: "#777", fill: "rgba(142, 143, 142, 1.000)",
stroke: "#000", stroke: "#000",
strokeWidth: 1, strokeWidth: 1,
}, },
@ -64,6 +63,10 @@ const ray = createShape<RayShape>({
return bounds return bounds
}, },
getCenter(shape) {
return shape.point
},
hitTest(shape, test) { hitTest(shape, test) {
return true return true
}, },

View file

@ -19,7 +19,7 @@ const rectangle = createShape<RectangleShape>({
size: [1, 1], size: [1, 1],
rotation: 0, rotation: 0,
style: { style: {
fill: "#777", fill: "rgba(142, 143, 142, 1.000)",
stroke: "#000", stroke: "#000",
}, },
...props, ...props,
@ -54,6 +54,11 @@ const rectangle = createShape<RectangleShape>({
return bounds return bounds
}, },
getCenter(shape) {
const bounds = this.getBounds(shape)
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
},
hitTest(shape) { hitTest(shape) {
return true return true
}, },

View file

@ -2,14 +2,16 @@ import translate from "./translate"
import transform from "./transform" import transform from "./transform"
import generate from "./generate" import generate from "./generate"
import createShape from "./create-shape" import createShape from "./create-shape"
import direction from "./direction" import direct from "./direct"
import rotate from "./rotate"
const commands = { const commands = {
translate, translate,
transform, transform,
generate, generate,
createShape, createShape,
direction, direct,
rotate,
} }
export default commands export default commands

32
state/commands/rotate.ts Normal file
View file

@ -0,0 +1,32 @@
import Command from "./command"
import history from "../history"
import { Data } from "types"
import { RotateSnapshot } from "state/sessions/rotate-session"
export default function translateCommand(
data: Data,
before: RotateSnapshot,
after: RotateSnapshot
) {
history.execute(
data,
new Command({
name: "translate_shapes",
category: "canvas",
do(data) {
const { shapes } = data.document.pages[after.currentPageId]
for (let { id, rotation } of after.shapes) {
shapes[id].rotation = rotation
}
},
undo(data) {
const { shapes } = data.document.pages[before.currentPageId]
for (let { id, rotation } of before.shapes) {
shapes[id].rotation = rotation
}
},
})
)
}

View file

@ -17,7 +17,7 @@ export const defaultDocument: Data["document"] = {
direction: [0.5, 0.5], direction: [0.5, 0.5],
style: { style: {
fill: "#AAA", fill: "#AAA",
stroke: "#777", stroke: "rgba(142, 143, 142, 1.000)",
strokeWidth: 1, strokeWidth: 1,
}, },
}), }),
@ -28,7 +28,7 @@ export const defaultDocument: Data["document"] = {
// point: [400, 500], // point: [400, 500],
// style: { // style: {
// fill: "#AAA", // fill: "#AAA",
// stroke: "#777", // stroke: "rgba(142, 143, 142, 1.000)",
// strokeWidth: 1, // strokeWidth: 1,
// }, // },
// }), // }),
@ -40,7 +40,7 @@ export const defaultDocument: Data["document"] = {
radius: 50, radius: 50,
style: { style: {
fill: "#AAA", fill: "#AAA",
stroke: "#777", stroke: "rgba(142, 143, 142, 1.000)",
strokeWidth: 1, strokeWidth: 1,
}, },
}), }),
@ -53,7 +53,7 @@ export const defaultDocument: Data["document"] = {
radiusY: 30, radiusY: 30,
style: { style: {
fill: "#AAA", fill: "#AAA",
stroke: "#777", stroke: "rgba(142, 143, 142, 1.000)",
strokeWidth: 1, strokeWidth: 1,
}, },
}), }),
@ -66,7 +66,7 @@ export const defaultDocument: Data["document"] = {
// radiusY: 30, // radiusY: 30,
// style: { // style: {
// fill: "#AAA", // fill: "#AAA",
// stroke: "#777", // stroke: "rgba(142, 143, 142, 1.000)",
// strokeWidth: 1, // strokeWidth: 1,
// }, // },
// }), // }),
@ -82,7 +82,7 @@ export const defaultDocument: Data["document"] = {
// ], // ],
// style: { // style: {
// fill: "none", // fill: "none",
// stroke: "#777", // stroke: "rgba(142, 143, 142, 1.000)",
// strokeWidth: 2, // strokeWidth: 2,
// strokeLinecap: "round", // strokeLinecap: "round",
// strokeLinejoin: "round", // strokeLinejoin: "round",
@ -96,7 +96,7 @@ export const defaultDocument: Data["document"] = {
size: [200, 200], size: [200, 200],
style: { style: {
fill: "#AAA", fill: "#AAA",
stroke: "#777", stroke: "rgba(142, 143, 142, 1.000)",
strokeWidth: 1, strokeWidth: 1,
}, },
}), }),
@ -108,7 +108,7 @@ export const defaultDocument: Data["document"] = {
// direction: [0.2, 0.2], // direction: [0.2, 0.2],
// style: { // style: {
// fill: "#AAA", // fill: "#AAA",
// stroke: "#777", // stroke: "rgba(142, 143, 142, 1.000)",
// strokeWidth: 1, // strokeWidth: 1,
// }, // },
// }), // }),

View file

@ -41,7 +41,7 @@ export default class DirectionSession extends BaseSession {
} }
complete(data: Data) { complete(data: Data) {
commands.direction(data, this.snapshot, getDirectionSnapshot(data)) commands.direct(data, this.snapshot, getDirectionSnapshot(data))
} }
} }

View file

@ -3,6 +3,7 @@ import BrushSession from "./brush-session"
import TranslateSession from "./translate-session" import TranslateSession from "./translate-session"
import TransformSession from "./transform-session" import TransformSession from "./transform-session"
import DirectionSession from "./direction-session" import DirectionSession from "./direction-session"
import RotateSession from "./rotate-session"
export { export {
BrushSession, BrushSession,
@ -10,4 +11,5 @@ export {
TranslateSession, TranslateSession,
TransformSession, TransformSession,
DirectionSession, DirectionSession,
RotateSession,
} }

View file

@ -0,0 +1,81 @@
import { Data } from "types"
import * as vec from "utils/vec"
import BaseSession from "./base-session"
import commands from "state/commands"
import { current } from "immer"
import { getCommonBounds } from "utils/utils"
import { getShapeUtils } from "lib/shapes"
export default class RotateSession extends BaseSession {
delta = [0, 0]
origin: number[]
snapshot: RotateSnapshot
constructor(data: Data, point: number[]) {
super(data)
this.origin = point
this.snapshot = getRotateSnapshot(data)
}
update(data: Data, point: number[]) {
const { currentPageId, center, shapes } = this.snapshot
const { document } = data
const a1 = vec.angle(center, this.origin)
const a2 = vec.angle(center, point)
for (let { id, rotation } of shapes) {
const shape = document.pages[currentPageId].shapes[id]
shape.rotation = rotation + ((a2 - a1) % (Math.PI * 2))
}
}
cancel(data: Data) {
const { document } = data
for (let shape of this.snapshot.shapes) {
document.pages[this.snapshot.currentPageId].shapes[shape.id].rotation =
shape.rotation
}
}
complete(data: Data) {
commands.rotate(data, this.snapshot, getRotateSnapshot(data))
}
}
export function getRotateSnapshot(data: Data) {
const {
selectedIds,
document: { pages },
currentPageId,
} = current(data)
const shapes = Array.from(selectedIds.values()).map(
(id) => pages[currentPageId].shapes[id]
)
// A mapping of selected shapes and their bounds
const shapesBounds = Object.fromEntries(
shapes.map((shape) => [shape.id, getShapeUtils(shape).getBounds(shape)])
)
// The common (exterior) bounds of the selected shapes
const bounds = getCommonBounds(...Object.values(shapesBounds))
const center = [
bounds.minX + bounds.width / 2,
bounds.minY + bounds.height / 2,
]
return {
currentPageId,
center,
shapes: shapes.map(({ id, rotation }) => ({
id,
rotation,
})),
}
}
export type RotateSnapshot = ReturnType<typeof getRotateSnapshot>

View file

@ -171,7 +171,8 @@ export default class TransformSession extends BaseSession {
const { shapes } = data.document.pages[currentPageId] const { shapes } = data.document.pages[currentPageId]
selectedIds.forEach((id) => { selectedIds.forEach((id) => {
const shape = shapes.shapes[id] const shape = shapes[id]
const { initialShape, initialShapeBounds } = shapeBounds[id] const { initialShape, initialShapeBounds } = shapeBounds[id]
getShapeUtils(shape).transform(shape, initialShapeBounds, { getShapeUtils(shape).transform(shape, initialShapeBounds, {

View file

@ -6,7 +6,6 @@ import {
PointerInfo, PointerInfo,
Shape, Shape,
ShapeType, ShapeType,
Shapes,
TransformCorner, TransformCorner,
TransformEdge, TransformEdge,
CodeControl, CodeControl,
@ -16,14 +15,14 @@ import shapeUtilityMap, { getShapeUtils } from "lib/shapes"
import history from "state/history" import history from "state/history"
import * as Sessions from "./sessions" import * as Sessions from "./sessions"
import commands from "./commands" import commands from "./commands"
import { controls } from "lib/code/control" import { updateFromCode } from "lib/code/generate"
import { generateFromCode, updateFromCode } from "lib/code/generate"
const initialData: Data = { const initialData: Data = {
isReadOnly: false, isReadOnly: false,
settings: { settings: {
fontSize: 13, fontSize: 13,
darkMode: false, isDarkMode: false,
isCodeOpen: false,
}, },
camera: { camera: {
point: [0, 0], point: [0, 0],
@ -56,6 +55,7 @@ const state = createState({
SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "line" }, SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "line" },
SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "polyline" }, SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "polyline" },
SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" }, SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" },
TOGGLED_CODE_PANEL_OPEN: "toggleCodePanel",
RESET_CAMERA: "resetCamera", RESET_CAMERA: "resetCamera",
}, },
initial: "loading", initial: "loading",
@ -64,236 +64,323 @@ const state = createState({
on: { on: {
MOUNTED: { MOUNTED: {
do: "restoreSavedData", do: "restoreSavedData",
to: "selecting", to: "ready",
}, },
}, },
}, },
selecting: { ready: {
on: { on: {
UNDO: { do: "undo" }, UNMOUNTED: [
REDO: { do: "redo" }, { unless: "isReadOnly", do: "forceSave" },
CANCELLED: { do: "clearSelectedIds" }, { to: "loading" },
DELETED: { do: "deleteSelectedIds" }, ],
SAVED_CODE: "saveCode",
GENERATED_FROM_CODE: ["setCodeControls", "setGeneratedShapes"],
INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
CHANGED_CODE_CONTROL: "updateControls",
}, },
initial: "notPointing", initial: "selecting",
states: { states: {
notPointing: { selecting: {
on: { on: {
POINTED_CANVAS: { to: "brushSelecting" }, SAVED: "forceSave",
POINTED_BOUNDS: { to: "pointingBounds" }, UNDO: { do: "undo" },
POINTED_BOUNDS_EDGE: { to: "transformingSelection" }, REDO: { do: "redo" },
POINTED_BOUNDS_CORNER: { to: "transformingSelection" }, CANCELLED: { do: "clearSelectedIds" },
MOVED_OVER_SHAPE: { DELETED: { do: "deleteSelectedIds" },
if: "pointHitsShape", SAVED_CODE: "saveCode",
then: { GENERATED_FROM_CODE: ["setCodeControls", "setGeneratedShapes"],
unless: "shapeIsHovered", INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
do: "setHoveredId", DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
}, CHANGED_CODE_CONTROL: "updateControls",
else: { if: "shapeIsHovered", do: "clearHoveredId" }, },
}, initial: "notPointing",
UNHOVERED_SHAPE: "clearHoveredId", states: {
POINTED_SHAPE: [ notPointing: {
"setPointedId", on: {
{ SELECTED_ALL: "selectAll",
if: "isPressingShiftKey", POINTED_CANVAS: { to: "brushSelecting" },
then: { POINTED_BOUNDS: { to: "pointingBounds" },
if: "isPointedShapeSelected", POINTED_BOUNDS_EDGE: { to: "transformingSelection" },
do: "pullPointedIdFromSelectedIds", POINTED_BOUNDS_CORNER: { to: "transformingSelection" },
else: { POINTED_ROTATE_HANDLE: { to: "rotatingSelection" },
do: "pushPointedIdToSelectedIds", MOVED_OVER_SHAPE: {
to: "pointingBounds", if: "pointHitsShape",
then: {
unless: "shapeIsHovered",
do: "setHoveredId",
}, },
else: { if: "shapeIsHovered", do: "clearHoveredId" },
}, },
else: [ UNHOVERED_SHAPE: "clearHoveredId",
POINTED_SHAPE: [
"setPointedId",
{ {
unless: "isPointedShapeSelected", if: "isPressingShiftKey",
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"], then: {
}, if: "isPointedShapeSelected",
{ do: "pullPointedIdFromSelectedIds",
to: "pointingBounds", else: {
do: "pushPointedIdToSelectedIds",
to: "pointingBounds",
},
},
else: [
{
unless: "isPointedShapeSelected",
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
},
{
to: "pointingBounds",
},
],
}, },
], ],
}, },
],
},
},
pointingBounds: {
on: {
STOPPED_POINTING: [
{
unless: ["isPointingBounds", "isPressingShiftKey"],
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
},
{ to: "notPointing" },
],
MOVED_POINTER: {
unless: "isReadOnly",
if: "distanceImpliesDrag",
to: "draggingSelection",
}, },
}, pointingBounds: {
},
transformingSelection: {
onEnter: "startTransformSession",
on: {
MOVED_POINTER: "updateTransformSession",
PANNED_CAMERA: "updateTransformSession",
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: { do: "cancelSession", to: "selecting" },
},
},
draggingSelection: {
onEnter: "startTranslateSession",
on: {
MOVED_POINTER: "updateTranslateSession",
PANNED_CAMERA: "updateTranslateSession",
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: { do: "cancelSession", to: "selecting" },
},
},
brushSelecting: {
onEnter: [
{ unless: "isPressingShiftKey", do: "clearSelectedIds" },
"startBrushSession",
],
on: {
MOVED_POINTER: "updateBrushSession",
PANNED_CAMERA: "updateBrushSession",
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: { do: "cancelSession", to: "selecting" },
},
},
},
},
dot: {
initial: "creating",
states: {
creating: {
on: {
POINTED_CANVAS: {
do: "createDot",
to: "dot.editing",
},
},
},
editing: {
on: {
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: {
do: ["cancelSession", "deleteSelectedIds"],
to: "selecting",
},
},
initial: "inactive",
states: {
inactive: {
on: { on: {
STOPPED_POINTING: [
{
unless: ["isPointingBounds", "isPressingShiftKey"],
do: ["clearSelectedIds", "pushPointedIdToSelectedIds"],
},
{ to: "notPointing" },
],
MOVED_POINTER: { MOVED_POINTER: {
unless: "isReadOnly",
if: "distanceImpliesDrag", if: "distanceImpliesDrag",
to: "dot.editing.active", to: "draggingSelection",
}, },
}, },
}, },
active: { rotatingSelection: {
onEnter: "startRotateSession",
on: {
MOVED_POINTER: "updateRotateSession",
PANNED_CAMERA: "updateRotateSession",
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: { do: "cancelSession", to: "selecting" },
},
},
transformingSelection: {
onEnter: "startTransformSession",
on: {
MOVED_POINTER: "updateTransformSession",
PANNED_CAMERA: "updateTransformSession",
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: { do: "cancelSession", to: "selecting" },
},
},
draggingSelection: {
onEnter: "startTranslateSession", onEnter: "startTranslateSession",
on: { on: {
MOVED_POINTER: "updateTranslateSession", MOVED_POINTER: "updateTranslateSession",
PANNED_CAMERA: "updateTranslateSession", PANNED_CAMERA: "updateTranslateSession",
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: { do: "cancelSession", to: "selecting" },
},
},
brushSelecting: {
onEnter: [
{ unless: "isPressingShiftKey", do: "clearSelectedIds" },
"startBrushSession",
],
on: {
MOVED_POINTER: "updateBrushSession",
PANNED_CAMERA: "updateBrushSession",
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: { do: "cancelSession", to: "selecting" },
}, },
}, },
}, },
}, },
}, dot: {
}, initial: "creating",
circle: {},
ellipse: {},
ray: {
initial: "creating",
states: {
creating: {
on: {
POINTED_CANVAS: {
do: "createRay",
to: "ray.editing",
},
},
},
editing: {
on: {
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: {
do: ["cancelSession", "deleteSelectedIds"],
to: "selecting",
},
},
initial: "inactive",
states: { states: {
inactive: { creating: {
on: { on: {
MOVED_POINTER: { POINTED_CANVAS: {
if: "distanceImpliesDrag", do: "createDot",
to: "ray.editing.active", to: "dot.editing",
}, },
}, },
}, },
active: { editing: {
onEnter: "startDirectionSession",
on: { on: {
MOVED_POINTER: "updateDirectionSession", STOPPED_POINTING: { do: "completeSession", to: "selecting" },
PANNED_CAMERA: "updateDirectionSession", CANCELLED: {
do: ["cancelSession", "deleteSelectedIds"],
to: "selecting",
},
}, },
}, initial: "inactive",
}, states: {
}, inactive: {
}, on: {
}, MOVED_POINTER: {
line: { if: "distanceImpliesDrag",
initial: "creating", to: "dot.editing.active",
states: { },
creating: { },
on: { },
POINTED_CANVAS: { active: {
do: "createLine", onEnter: "startTranslateSession",
to: "line.editing", on: {
}, MOVED_POINTER: "updateTranslateSession",
}, PANNED_CAMERA: "updateTranslateSession",
}, },
editing: {
on: {
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: {
do: ["cancelSession", "deleteSelectedIds"],
to: "selecting",
},
},
initial: "inactive",
states: {
inactive: {
on: {
MOVED_POINTER: {
if: "distanceImpliesDrag",
to: "line.editing.active",
}, },
}, },
}, },
active: { },
onEnter: "startDirectionSession", },
circle: {
initial: "creating",
states: {
creating: {
on: { on: {
MOVED_POINTER: "updateDirectionSession", POINTED_CANVAS: {
PANNED_CAMERA: "updateDirectionSession", to: "circle.editing",
},
},
},
editing: {
on: {
STOPPED_POINTING: { to: "selecting" },
CANCELLED: { to: "selecting" },
MOVED_POINTER: {
if: "distanceImpliesDrag",
do: "createCircle",
to: "drawingShape.bounds",
},
}, },
}, },
}, },
}, },
ellipse: {
initial: "creating",
states: {
creating: {
on: {
POINTED_CANVAS: {
to: "ellipse.editing",
},
},
},
editing: {
on: {
STOPPED_POINTING: { to: "selecting" },
CANCELLED: { to: "selecting" },
MOVED_POINTER: {
if: "distanceImpliesDrag",
do: "createEllipse",
to: "drawingShape.bounds",
},
},
},
},
},
rectangle: {
initial: "creating",
states: {
creating: {
on: {
POINTED_CANVAS: {
to: "rectangle.editing",
},
},
},
editing: {
on: {
STOPPED_POINTING: { to: "selecting" },
CANCELLED: { to: "selecting" },
MOVED_POINTER: {
if: "distanceImpliesDrag",
do: "createRectangle",
to: "drawingShape.bounds",
},
},
},
},
},
ray: {
initial: "creating",
states: {
creating: {
on: {
POINTED_CANVAS: {
do: "createRay",
to: "ray.editing",
},
},
},
editing: {
on: {
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: {
do: ["cancelSession", "deleteSelectedIds"],
to: "selecting",
},
MOVED_POINTER: {
if: "distanceImpliesDrag",
to: "drawingShape.direction",
},
},
},
},
},
line: {
initial: "creating",
states: {
creating: {
on: {
POINTED_CANVAS: {
do: "createLine",
to: "line.editing",
},
},
},
editing: {
on: {
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: {
do: ["cancelSession", "deleteSelectedIds"],
to: "selecting",
},
MOVED_POINTER: {
if: "distanceImpliesDrag",
to: "drawingShape.direction",
},
},
},
},
},
polyline: {},
},
},
drawingShape: {
on: {
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: {
do: ["cancelSession", "deleteSelectedIds"],
to: "selecting",
},
},
initial: "drawingShapeBounds",
states: {
bounds: {
onEnter: "startDrawTransformSession",
on: {
MOVED_POINTER: "updateTransformSession",
PANNED_CAMERA: "updateTransformSession",
},
},
direction: {
onEnter: "startDirectionSession",
on: {
MOVED_POINTER: "updateDirectionSession",
PANNED_CAMERA: "updateDirectionSession",
},
},
}, },
}, },
polyline: {},
rectangle: {},
}, },
conditions: { conditions: {
isPointingBounds(data, payload: PointerInfo) { isPointingBounds(data, payload: PointerInfo) {
@ -358,6 +445,36 @@ const state = createState({
data.selectedIds.add(shape.id) data.selectedIds.add(shape.id)
}, },
createCircle(data, payload: PointerInfo) {
const shape = shapeUtilityMap[ShapeType.Circle].create({
point: screenToWorld(payload.point, data),
radius: 1,
})
commands.createShape(data, shape)
data.selectedIds.add(shape.id)
},
createEllipse(data, payload: PointerInfo) {
const shape = shapeUtilityMap[ShapeType.Ellipse].create({
point: screenToWorld(payload.point, data),
radiusX: 1,
radiusY: 1,
})
commands.createShape(data, shape)
data.selectedIds.add(shape.id)
},
createRectangle(data, payload: PointerInfo) {
const shape = shapeUtilityMap[ShapeType.Rectangle].create({
point: screenToWorld(payload.point, data),
size: [1, 1],
})
commands.createShape(data, shape)
data.selectedIds.add(shape.id)
},
/* -------------------- Sessions -------------------- */ /* -------------------- Sessions -------------------- */
// Shared // Shared
@ -381,6 +498,17 @@ const state = createState({
session.update(data, screenToWorld(payload.point, data)) session.update(data, screenToWorld(payload.point, data))
}, },
// Rotating
startRotateSession(data, payload: PointerInfo) {
session = new Sessions.RotateSession(
data,
screenToWorld(payload.point, data)
)
},
updateRotateSession(data, payload: PointerInfo) {
session.update(data, screenToWorld(payload.point, data))
},
// Dragging / Translating // Dragging / Translating
startTranslateSession(data, payload: PointerInfo) { startTranslateSession(data, payload: PointerInfo) {
session = new Sessions.TranslateSession( session = new Sessions.TranslateSession(
@ -403,6 +531,13 @@ const state = createState({
screenToWorld(payload.point, data) screenToWorld(payload.point, data)
) )
}, },
startDrawTransformSession(data, payload: PointerInfo) {
session = new Sessions.TransformSession(
data,
TransformCorner.BottomRight,
screenToWorld(payload.point, data)
)
},
updateTransformSession(data, payload: PointerInfo) { updateTransformSession(data, payload: PointerInfo) {
session.update(data, screenToWorld(payload.point, data)) session.update(data, screenToWorld(payload.point, data))
}, },
@ -420,6 +555,13 @@ const state = createState({
/* -------------------- Selection ------------------- */ /* -------------------- Selection ------------------- */
selectAll(data) {
const { selectedIds, document, currentPageId } = data
selectedIds.clear()
for (let id in document.pages[currentPageId].shapes) {
selectedIds.add(id)
}
},
setHoveredId(data, payload: PointerInfo) { setHoveredId(data, payload: PointerInfo) {
data.hoveredId = payload.target data.hoveredId = payload.target
}, },
@ -488,9 +630,12 @@ const state = createState({
data.selectedIds.clear() data.selectedIds.clear()
}, },
/* ---------------------- Misc ---------------------- */ /* ---------------------- History ---------------------- */
// History // History
forceSave(data) {
history.save(data)
},
enableHistory() { enableHistory() {
history.enable() history.enable()
}, },
@ -504,7 +649,16 @@ const state = createState({
history.redo(data) history.redo(data)
}, },
// Code /* ---------------------- Code ---------------------- */
closeCodePanel(data) {
data.settings.isCodeOpen = false
},
openCodePanel(data) {
data.settings.isCodeOpen = true
},
toggleCodePanel(data) {
data.settings.isCodeOpen = !data.settings.isCodeOpen
},
setGeneratedShapes( setGeneratedShapes(
data, data,
payload: { shapes: Shape[]; controls: CodeControl[] } payload: { shapes: Shape[]; controls: CodeControl[] }

View file

@ -10,7 +10,8 @@ export interface Data {
isReadOnly: boolean isReadOnly: boolean
settings: { settings: {
fontSize: number fontSize: number
darkMode: boolean isDarkMode: boolean
isCodeOpen: boolean
} }
camera: { camera: {
point: number[] point: number[]