Skip to content

Commit e236f06

Browse files
authored
Merge pull request #1 from mustafacagri/feat/unlimited-path-drawing
feat: enhance path drawing with flexible handling and live updates
2 parents 5969d1c + 97ff682 commit e236f06

File tree

6 files changed

+108
-85
lines changed

6 files changed

+108
-85
lines changed

β€Žsrc/App.tsxβ€Ž

Lines changed: 68 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,31 @@ import { getRandomColor } from './utils'
55
// @ts-expect-error -> no support for EaselJS in TypeScript -> https://github.com/CreateJS/EaselJS/issues/796
66
import { Stage, Shape, Ticker } from '@createjs/easeljs'
77
import { Shape as ShapeInterface } from './interfaces'
8-
import { ShapeType } from './utils/constants'
8+
import { defaultPathThickness, ShapeType } from './utils/constants'
99
import Sidebar from './components/Sidebar'
1010
import ThreeJSViewer from './components/ThreeJSViewer'
11+
import { isEmpty } from 'lodash'
1112

1213
const Canvas: React.FC = () => {
13-
const pathThickness = 25
14-
14+
const [pathThickness, setPathThickness] = useState(defaultPathThickness)
1515
const canvasRef = useRef<HTMLCanvasElement>(null)
1616
const stageRef = useRef<Stage | null>(null)
1717
const [shapes, setShapes] = useState<ShapeInterface[]>([])
1818
const [selectedShape, setSelectedShape] = useState<ShapeInterface | null>(null)
1919
const [shapeType, setShapeType] = useState<ShapeType>('rectangle')
20-
const [currentPath, setCurrentPath] = useState<Shape | null>(null)
21-
const [pathPoints, setPathPoints] = useState<{ x: number; y: number }[]>([])
2220
const [isDrawing, setIsDrawing] = useState(false)
2321
const [startPoint, setStartPoint] = useState<{ x: number; y: number } | null>(null)
2422
const [currentShape, setCurrentShape] = useState<Shape | null>(null)
2523
const [is3DMode, setIs3DMode] = useState(false)
2624
const pathColor = useRef(getRandomColor())
2725

2826
const selectedIdRef = useRef<number | undefined>(undefined)
27+
const pathPointsRef = useRef<{ x: number; y: number }[]>([])
2928

3029
const toggleViewMode = () => setIs3DMode(!is3DMode)
3130

31+
const getStrokeThickness = (thickness?: number) => thickness ?? pathThickness
32+
3233
// Function to handle shape creation
3334
const createShape = useCallback(
3435
(props: ShapeInterface) => {
@@ -49,12 +50,13 @@ const Canvas: React.FC = () => {
4950
if (props.endX === undefined || props.endY === undefined) return
5051

5152
g.beginStroke(props.strokeColor)
53+
.setStrokeStyle(getStrokeThickness(props?.thickness))
5254
.moveTo(0, 0)
5355
.lineTo(props.endX - props.x, props.endY - props.y)
5456
break
5557
case 'path':
5658
if (props?.points && props.points?.length > 1) {
57-
g.beginStroke(props.strokeColor).setStrokeStyle(pathThickness)
59+
g.beginStroke(props.strokeColor).setStrokeStyle(getStrokeThickness(props?.thickness))
5860
g.moveTo(props.points[0].x, props.points[0].y)
5961

6062
props.points.forEach((point: { x: number; y: number }, index: number) => {
@@ -102,6 +104,7 @@ const Canvas: React.FC = () => {
102104
const g = this.graphics
103105
g.clear()
104106
.beginStroke(props.strokeColor)
107+
.setStrokeStyle(getStrokeThickness(props?.thickness))
105108
.moveTo(0, 0)
106109
.lineTo(props.endX - props.x, props.endY - props.y)
107110
} else if (props.type === 'path' && props.points) {
@@ -163,20 +166,38 @@ const Canvas: React.FC = () => {
163166
const g = currentShape.graphics
164167
g.clear()
165168

169+
const { x: startX, y: startY } = startPoint
170+
let thickness = pathThickness
171+
172+
if (['line', 'path'].includes(shapeType) && currentShape?.graphics?._strokeStyle?.width) {
173+
thickness = currentShape?.graphics?._strokeStyle?.width
174+
}
175+
166176
switch (shapeType) {
167177
case 'rectangle':
168178
g.beginFill(pathColor.current)
169179
.beginStroke(pathColor.current)
170-
.drawRect(startPoint.x, startPoint.y, x - startPoint.x, y - startPoint.y)
180+
.drawRect(startX, startY, x - startX, y - startY)
171181
break
172182
case 'circle': {
173-
const radius = Math.sqrt(Math.pow(x - startPoint.x, 2) + Math.pow(y - startPoint.y, 2))
174-
g.beginFill(pathColor.current).beginStroke(pathColor.current).drawCircle(startPoint.x, startPoint.y, radius)
183+
const radius = Math.sqrt(Math.pow(x - startX, 2) + Math.pow(y - startY, 2))
184+
g.beginFill(pathColor.current).beginStroke(pathColor.current).drawCircle(startX, startY, radius)
175185
break
176186
}
177187
case 'line':
178-
g.beginStroke(pathColor.current).moveTo(startPoint.x, startPoint.y).lineTo(x, y)
188+
g.beginStroke(pathColor.current).setStrokeStyle(thickness).moveTo(startX, startY).lineTo(x, y)
179189
break
190+
case 'path': {
191+
const newPoints = g.beginStroke(pathColor.current).setStrokeStyle(thickness)
192+
193+
if (isEmpty(pathPointsRef.current)) {
194+
newPoints.moveTo(startX, startY)
195+
} else {
196+
pathPointsRef.current.forEach(point => newPoints.lineTo(point.x, point.y))
197+
}
198+
199+
newPoints.lineTo(x, y)
200+
}
180201
}
181202

182203
stageRef.current?.update()
@@ -214,11 +235,12 @@ const Canvas: React.FC = () => {
214235
shapeProps = {
215236
type: 'line',
216237
fillColor: 'transparent',
217-
strokeColor: getRandomColor(),
238+
strokeColor: pathColor.current,
218239
x: startPoint.x,
219240
y: startPoint.y,
220241
endX: x,
221242
endY: y,
243+
thickness: pathThickness,
222244
}
223245
break
224246
default:
@@ -233,57 +255,21 @@ const Canvas: React.FC = () => {
233255
stageRef.current?.update()
234256
}
235257

236-
const updatePath = (x: number, y: number) => {
237-
if (currentPath && pathPoints.length > 0) {
238-
const g = currentPath.graphics
239-
g.clear().beginStroke(pathColor.current).setStrokeStyle(pathThickness)
240-
g.moveTo(pathPoints[0].x, pathPoints[0].y)
241-
g.lineTo(x, y) // Draw the latest line to the current mouse point
242-
243-
stageRef.current?.update() // Refresh stage
244-
}
245-
}
246-
247-
const endPath = (x: number, y: number) => {
248-
if (currentPath && pathPoints.length > 0) {
249-
const newPoints = [...pathPoints, { x, y }]
250-
251-
createShape({
252-
type: 'path',
253-
fillColor: 'transparent',
254-
strokeColor: pathColor.current,
255-
x: 0,
256-
y: 0,
257-
points: newPoints,
258-
})
259-
260-
stageRef.current?.removeChild(currentPath)
261-
setPathPoints([])
262-
setCurrentPath(null)
263-
setIsDrawing(false)
264-
stageRef.current?.update()
265-
}
266-
}
267-
268258
// Mouse event handlers
269259
const handleCanvasMouseDown = useCallback(
270260
(event: React.MouseEvent<HTMLCanvasElement>) => {
271-
pathColor.current = getRandomColor()
272-
261+
if (!isDrawing) pathColor.current = getRandomColor()
273262
const { offsetX, offsetY } = event.nativeEvent
263+
264+
if (shapeType === 'path') {
265+
pathPointsRef.current.push({ x: offsetX, y: offsetY })
266+
}
267+
274268
const clickedShape = stageRef.current?.getObjectsUnderPoint(offsetX, offsetY, 1)?.[0] as ShapeInterface
275269

276270
if (clickedShape) {
277271
selectedIdRef.current = clickedShape?.id as number
278-
279272
setSelectedShape(clickedShape)
280-
} else if (shapeType === 'path') {
281-
if (!isDrawing) {
282-
startPath(offsetX, offsetY)
283-
} else {
284-
endPath(offsetX, offsetY)
285-
startPath(offsetX, offsetY)
286-
}
287273
} else {
288274
startDrawing(offsetX, offsetY)
289275
}
@@ -296,11 +282,7 @@ const Canvas: React.FC = () => {
296282
const { offsetX, offsetY } = event.nativeEvent
297283

298284
if (isDrawing) {
299-
if (shapeType === 'path') {
300-
updatePath(offsetX, offsetY)
301-
} else {
302-
draw(offsetX, offsetY)
303-
}
285+
draw(offsetX, offsetY)
304286
}
305287
},
306288
[isDrawing, shapeType]
@@ -312,7 +294,7 @@ const Canvas: React.FC = () => {
312294

313295
if (isDrawing) {
314296
if (shapeType === 'path') {
315-
endPath(offsetX, offsetY)
297+
pathPointsRef.current = [...pathPointsRef.current, { x: offsetX, y: offsetY }]
316298
} else {
317299
endDrawing(offsetX, offsetY)
318300
}
@@ -339,16 +321,6 @@ const Canvas: React.FC = () => {
339321
[isDrawing, shapeType]
340322
)
341323

342-
const startPath = (x: number, y: number) => {
343-
const newPath = new Shape()
344-
newPath.graphics.beginStroke(pathColor.current).setStrokeStyle(pathThickness)
345-
stageRef.current?.addChild(newPath)
346-
347-
setCurrentPath(newPath)
348-
setPathPoints([{ x, y }])
349-
setIsDrawing(true) // Ensure we are in drawing mode
350-
}
351-
352324
// Initialize canvas and attach event handlers
353325
useEffect(() => {
354326
if (canvasRef.current) {
@@ -380,9 +352,8 @@ const Canvas: React.FC = () => {
380352

381353
setShapes([])
382354
setSelectedShape(null)
383-
setCurrentPath(null)
384-
setPathPoints([])
385355
setIsDrawing(false)
356+
pathPointsRef.current = []
386357
}, [])
387358

388359
// Keyboard delete, backspace handlers
@@ -411,28 +382,42 @@ const Canvas: React.FC = () => {
411382
event.preventDefault()
412383

413384
if (isDrawing) {
385+
if (shapeType === 'path' && Array.isArray(pathPointsRef.current) && pathPointsRef.current.length > 1) {
386+
const current = {
387+
type: 'path',
388+
fillColor: pathColor.current,
389+
strokeColor: pathColor.current,
390+
x: 0,
391+
y: 0,
392+
points: pathPointsRef.current,
393+
instance: stageRef.current?.children[stageRef.current?.children.length - 1] as Shape,
394+
thickness: pathThickness,
395+
}
396+
397+
stageRef.current?.removeChild(currentShape)
398+
stageRef.current?.update()
399+
400+
createShape({
401+
...current,
402+
})
403+
404+
pathPointsRef.current = []
405+
setStartPoint(null)
406+
}
407+
414408
setIsDrawing(false)
415-
setCurrentPath(null)
416-
setPathPoints([])
417409
}
418410
}
419411

420412
window.addEventListener('contextmenu', handleRightClick)
421413

422-
if (!isDrawing) {
423-
setCurrentPath(null)
424-
setPathPoints([])
425-
}
426-
427414
return () => {
428415
window.removeEventListener('contextmenu', handleRightClick)
429416
}
430417
}, [isDrawing])
431418

432419
useEffect(() => {
433-
if (shapeType !== 'path') {
434-
setIsDrawing(false)
435-
}
420+
setIsDrawing(false)
436421
}, [shapeType])
437422

438423
return (
@@ -446,6 +431,8 @@ const Canvas: React.FC = () => {
446431
shapeType={shapeType}
447432
toggleViewMode={toggleViewMode}
448433
is3DMode={is3DMode}
434+
pathThickness={pathThickness}
435+
setPathThickness={setPathThickness}
449436
/>
450437
<div
451438
id='CanvasContainer'

β€Žsrc/components/Sidebar.tsxβ€Ž

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { importShapes, exportShapes } from '../utils/exportimport'
22
import { isEmpty } from 'lodash'
3-
import { shapeList } from '../utils/constants'
3+
import { defaultPathThickness, shapeList } from '../utils/constants'
44
import { SidebarProps } from '../interfaces'
55
import Button from './ui/Button'
66

@@ -12,11 +12,18 @@ export default function Sidebar({
1212
shapeType,
1313
toggleViewMode,
1414
is3DMode,
15+
pathThickness,
16+
setPathThickness,
1517
}: Readonly<SidebarProps>) {
1618
const importFunc = (event: React.ChangeEvent<HTMLInputElement>) => {
1719
importShapes({ event, createShape, clearShapes })
1820
}
1921

22+
const setThickness = (thickness: number) => {
23+
if (thickness < 1 || isNaN(thickness)) thickness = defaultPathThickness
24+
25+
setPathThickness(thickness)
26+
}
2027
const iconList = ['rectangle', 'circle', 'horizontal_rule', 'route']
2128

2229
return (
@@ -34,6 +41,19 @@ export default function Sidebar({
3441
<span className='material-icons mr-2'>{iconList[index]}</span>
3542
{type.charAt(0).toUpperCase() + type.slice(1)}
3643
</Button>
44+
45+
{['line', 'path'].includes(type) && shapeType === type && (
46+
<div className='w-full my-4 flex items-center justify-between'>
47+
<span className='text-gray-500 text-xs'>Path thickness:</span>
48+
<input
49+
type='number'
50+
value={pathThickness}
51+
onChange={e => setThickness(Number(e.target.value))}
52+
className='p-2 border border-gray-200 rounded-md focus:outline-none focus:shadow-outline w-32'
53+
placeholder='Path thickness'
54+
/>
55+
</div>
56+
)}
3757
</li>
3858
))}
3959
</ul>

β€Žsrc/components/ThreeJSViewer.tsxβ€Ž

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useRef, useEffect } from 'react'
22
import * as THREE from 'three'
33
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
44
import { Shape as ShapeInterface } from '../interfaces'
5+
import { defaultPathThickness } from '../utils/constants'
56

67
interface ThreeJSViewerProps {
78
shapes: ShapeInterface[]
@@ -49,6 +50,8 @@ const ThreeJSViewer: React.FC<ThreeJSViewerProps> = ({ shapes }) => {
4950
let material: THREE.Material
5051
let mesh: THREE.Mesh
5152

53+
const thickness = shape?.thickness ?? shape?.instance?._strokeStyle?.width ?? defaultPathThickness
54+
5255
switch (shape.type) {
5356
case 'rectangle': {
5457
if (shape.width === undefined || shape.height === undefined) {
@@ -75,7 +78,7 @@ const ThreeJSViewer: React.FC<ThreeJSViewerProps> = ({ shapes }) => {
7578
const startPosition = convertTo3DCoords(shape.x, shape.y)
7679
const endPosition = convertTo3DCoords(shape.endX!, shape.endY!)
7780
const lineCurve = new THREE.LineCurve3(startPosition, endPosition)
78-
geometry = new THREE.TubeGeometry(lineCurve, 10, 2, 8, false)
81+
geometry = new THREE.TubeGeometry(lineCurve, 10, thickness, 8, false)
7982
material = new THREE.MeshPhongMaterial({ color: shape?.strokeColor ?? 0x000000 })
8083
mesh = new THREE.Mesh(geometry, material)
8184
break
@@ -84,7 +87,7 @@ const ThreeJSViewer: React.FC<ThreeJSViewerProps> = ({ shapes }) => {
8487
if (shape.points && shape.points.length > 1) {
8588
const pathPoints = shape.points.map(point => convertTo3DCoords(point.x, point.y))
8689
const curve = new THREE.CatmullRomCurve3(pathPoints)
87-
geometry = new THREE.TubeGeometry(curve, 64, 25, 8, true)
90+
geometry = new THREE.TubeGeometry(curve, 64, thickness, 8, false)
8891
material = new THREE.MeshPhongMaterial({ color: shape.strokeColor || 0x000000 })
8992
mesh = new THREE.Mesh(geometry, material)
9093
} else {

β€Žsrc/interfaces/index.tsβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface Shape {
1616
endY?: number
1717
graphics?: any
1818
instance?: any
19+
thickness?: number
1920
}
2021

2122
export interface SidebarProps {
@@ -26,4 +27,6 @@ export interface SidebarProps {
2627
shapeType: string
2728
toggleViewMode: () => void
2829
is3DMode: boolean
30+
pathThickness: number
31+
setPathThickness: (thickness: number) => void
2932
}

β€Žsrc/utils/constants.tsβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export const shapeList = ['rectangle', 'circle', 'line', 'path']
22
export type ShapeType = (typeof shapeList)[number]
3+
export const defaultPathThickness = 25

0 commit comments

Comments
Β (0)