Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/features/preview/PreviewManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@ import React, { useRef } from "react"
import { useSelector, useDispatch } from "react-redux"
import Select from "react-select"
import Slider from "rc-slider"
import Button from "react-bootstrap/Button"
import "rc-slider/assets/index.css"
import { FaPencilAlt } from "react-icons/fa"
import {
updateEffect,
selectCurrentEffect,
} from "@/features/effects/effectsSlice"
import {
selectPreviewSliderValue,
selectPreviewZoom,
selectDrawingMode,
} from "@/features/preview/previewSlice"
import { updateLayer, selectCurrentLayer } from "@/features/layers/layersSlice"
import { getShape } from "@/features/shapes/shapeFactory"
import { getEffect } from "@/features/effects/effectFactory"
import "./PreviewManager.scss"
import { updatePreview } from "./previewSlice"
import {
updatePreview,
toggleDrawingMode,
exitDrawingMode,
} from "./previewSlice"
import PreviewWindow from "./PreviewWindow"

const PreviewManager = ({ isActive }) => {
Expand All @@ -26,6 +33,7 @@ const PreviewManager = ({ isActive }) => {
const currentEffectLayer = useSelector(selectCurrentEffect)
const sliderValue = useSelector(selectPreviewSliderValue)
const zoom = useSelector(selectPreviewZoom)
const drawingMode = useSelector(selectDrawingMode)
const wrapperRef = useRef()

const currentShape = getShape(currentLayer?.type || "polygon")
Expand All @@ -48,6 +56,10 @@ const PreviewManager = ({ isActive }) => {
dispatch(updatePreview({ zoom: option.value }))
}

const handleDrawToggle = () => {
dispatch(toggleDrawingMode())
}

const arrowKeyChange = (layer, event) => {
if (!layer) {
return
Expand Down Expand Up @@ -77,6 +89,11 @@ const PreviewManager = ({ isActive }) => {
}

const handleKeyDown = (event) => {
if (event.key === "Escape" && drawingMode) {
dispatch(exitDrawingMode())
return
}

if (currentLayer) {
if (currentShape.canMove(currentLayer)) {
const attrs = arrowKeyChange(currentLayer, event)
Expand Down Expand Up @@ -115,6 +132,16 @@ const PreviewManager = ({ isActive }) => {

<div className="mt-auto py-2 bg-white d-flex align-items-center">
<div className="mx-2">
<Button
variant={drawingMode ? "primary" : "outline-secondary"}
size="sm"
onClick={handleDrawToggle}
title={drawingMode ? "Exit draw mode (Esc)" : "Draw on canvas"}
>
<FaPencilAlt />
</Button>
</div>
<div className="mx-1">
<Select
id="zoom-select"
options={zoomChoices}
Expand Down
95 changes: 90 additions & 5 deletions src/features/preview/PreviewWindow.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
/* global document, getComputedStyle, window */

import React, { useEffect, useRef } from "react"
import React, { useCallback, useEffect, useRef, useState } from "react"
import { useSelector, useDispatch } from "react-redux"
import { isEqual } from "lodash"
import { Stage, Layer, Circle, Rect } from "react-konva"
import { Stage, Layer, Circle, Rect, Line } from "react-konva"
import throttle from "lodash/throttle"
import { selectPreviewState } from "@/features/preview/previewSlice"
import { selectCurrentMachine } from "@/features/machines/machinesSlice"
import { getMachine } from "@/features/machines/machineFactory"
import {
addLayer,
selectSelectedLayer,
selectVisibleLayerIds,
setCurrentLayer,
} from "@/features/layers/layersSlice"
import ShapePreview from "./ShapePreview"
import ConnectorPreview from "./ConnectorPreview"
import { setPreviewSize, selectPreviewZoom } from "./previewSlice"
import {
setPreviewSize,
selectPreviewZoom,
selectDrawingMode,
selectDrawingPoints,
addDrawingPoint,
exitDrawingMode,
clearDrawingPoints,
} from "./previewSlice"
import LayerFactory from "@/features/layers/Layer"

const PreviewWindow = ({ isActive }) => {
const dispatch = useDispatch()
Expand All @@ -25,10 +35,14 @@ const PreviewWindow = ({ isActive }) => {
const selectedLayer = useSelector(selectSelectedLayer, isEqual)
const layerIds = useSelector(selectVisibleLayerIds, isEqual)
const zoom = useSelector(selectPreviewZoom)
const drawingMode = useSelector(selectDrawingMode)
const drawingPoints = useSelector(selectDrawingPoints)
const [isDrawing, setIsDrawing] = useState(false)
const stageZoom = zoom > 1 ? zoom : 1
const offsetZoom = zoom > 1 ? 1 : zoom
const remainingLayerIds = layerIds.filter((id) => id !== selectedLayer?.id)
const layerRef = useRef()
const stageRef = useRef()
const stagePadding = 22

useEffect(() => {
Expand Down Expand Up @@ -70,20 +84,75 @@ const PreviewWindow = ({ isActive }) => {
? layerIds[selectedIdx + 1]
: null

// add hidden debugging option to toggle the hit canvas on the layer when the user
// clicks on the layer while pressing the Alt key
// Get machine coordinates from a pointer event
const getPointerMachineCoords = useCallback(() => {
const stage = stageRef.current
if (!stage) return null
const pos = stage.getRelativePointerPosition()
if (!pos) return null
return { x: pos.x, y: -pos.y } // flip Y: Konva Y-down → Sandify Y-up
}, [])

// Drawing event handlers
const handleDrawStart = useCallback(
(e) => {
if (!drawingMode) return
e.cancelBubble = true
setIsDrawing(true)
dispatch(clearDrawingPoints())
const pt = getPointerMachineCoords()
if (pt) dispatch(addDrawingPoint(pt))
},
[drawingMode, dispatch, getPointerMachineCoords],
)

const handleDrawMove = useCallback(
(e) => {
if (!drawingMode || !isDrawing) return
e.cancelBubble = true
const pt = getPointerMachineCoords()
if (pt) dispatch(addDrawingPoint(pt))
},
[drawingMode, isDrawing, dispatch, getPointerMachineCoords],
)

const handleDrawEnd = useCallback(() => {
if (!drawingMode || !isDrawing) return
setIsDrawing(false)

if (drawingPoints.length >= 2) {
const layer = new LayerFactory("drawing")
const attrs = layer.getInitialState({
machine,
drawingPoints,
})
attrs.name = "Drawing"
dispatch(addLayer(attrs))
}

dispatch(exitDrawingMode())
}, [drawingMode, isDrawing, drawingPoints, machine, dispatch])

// Normal click handler (deselect layers)
const handleStageClick = (e) => {
if (drawingMode) return
dispatch(setCurrentLayer(null))
if (e.evt.altKey && layerRef.current) {
layerRef.current.toggleHitCanvas()
e.cancelBubble = true
}
}

// Convert drawing points to flat [x1,y1,x2,y2,...] for Konva Line
const drawingLinePoints = drawingPoints.flatMap((p) => [p.x, -p.y]) // flip Y back for Konva rendering

const cursorStyle = drawingMode ? "crosshair" : "default"

// some awkward rendering to put the current layer as the last child in the layer to ensure
// transformer rotation works; this is a Konva restriction.
return (
<Stage
ref={stageRef}
scaleX={scale * zoom}
scaleY={scale * zoom}
height={height * scaleHeight * stageZoom + stagePadding}
Expand All @@ -95,6 +164,13 @@ const PreviewWindow = ({ isActive }) => {
(-height * (scaleHeight / scale / offsetZoom) - stagePadding * 0.5) / 2
}
onClick={handleStageClick}
onMouseDown={handleDrawStart}
onMouseMove={handleDrawMove}
onMouseUp={handleDrawEnd}
onTouchStart={handleDrawStart}
onTouchMove={handleDrawMove}
onTouchEnd={handleDrawEnd}
style={{ cursor: cursorStyle }}
>
<Layer ref={layerRef}>
{machine.type === "polar" && (
Expand Down Expand Up @@ -154,6 +230,15 @@ const PreviewWindow = ({ isActive }) => {
]
.flat()
.filter((e) => e !== null)}
{drawingMode && drawingPoints.length > 0 && (
<Line
points={drawingLinePoints}
stroke="#00ff00"
strokeWidth={1 / (scale * zoom)}
lineCap="round"
lineJoin="round"
/>
)}
</Layer>
</Stage>
)
Expand Down
35 changes: 34 additions & 1 deletion src/features/preview/previewSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const previewSlice = createSlice({
canvasHeight: 600,
sliderValue: 0.0,
zoom: 1.0,
drawingMode: false,
drawingPoints: [],
},
reducers: {
updatePreview(state, action) {
Expand All @@ -22,10 +24,31 @@ const previewSlice = createSlice({
state.canvasHeight = action.payload.height
state.canvasWidth = action.payload.width
},
toggleDrawingMode(state) {
state.drawingMode = !state.drawingMode
state.drawingPoints = []
},
exitDrawingMode(state) {
state.drawingMode = false
state.drawingPoints = []
},
addDrawingPoint(state, action) {
state.drawingPoints.push(action.payload)
},
clearDrawingPoints(state) {
state.drawingPoints = []
},
},
})

export const { updatePreview, setPreviewSize } = previewSlice.actions
export const {
updatePreview,
setPreviewSize,
toggleDrawingMode,
exitDrawingMode,
addDrawingPoint,
clearDrawingPoints,
} = previewSlice.actions
export default previewSlice.reducer

// ------------------------------
Expand All @@ -46,3 +69,13 @@ export const selectPreviewZoom = createSelector(
selectPreviewState,
(state) => state.zoom,
)

export const selectDrawingMode = createSelector(
selectPreviewState,
(state) => state.drawingMode ?? false,
)

export const selectDrawingPoints = createSelector(
selectPreviewState,
(state) => state.drawingPoints ?? [],
)
2 changes: 2 additions & 0 deletions src/features/preview/previewSlice.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ describe("preview reducer", () => {
canvasHeight: 600,
sliderValue: 0,
zoom: 1,
drawingMode: false,
drawingPoints: [],
})
})

Expand Down
Loading