Last active
November 29, 2024 12:17
-
-
Save sjdonado/7be445f4812c578fe8b176c1eb8923b0 to your computer and use it in GitHub Desktop.
Fabric 2d editor prototype
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef } from 'react'; | |
import { fabric } from 'fabric'; | |
import { useCanvas } from './useCanvas'; | |
const PVEditor = () => { | |
const canvasRef = useRef(null); | |
const { state, addObject, updateObject, deleteObject, selectObject, syncActiveObject } = useCanvas(); | |
useEffect(() => { | |
const canvas = new fabric.Canvas('canvas', { width: 800, height: 600 }); | |
canvasRef.current = canvas; | |
canvas.on('selection:updated', () => syncActiveObject(canvas)); | |
canvas.on('selection:cleared', () => syncActiveObject(canvas)); | |
return () => canvas.dispose(); | |
}, [syncActiveObject]); | |
useEffect(() => { | |
if (!canvasRef.current) return; | |
const canvas = canvasRef.current; | |
canvas.clear(); | |
state.objects.forEach((obj) => canvas.add(obj)); | |
}, [state.objects]); | |
return ( | |
<div> | |
<canvas id="canvas" /> | |
<button | |
onClick={() => addObject()} | |
> | |
Add Roof Area | |
</button> | |
<button | |
onClick={() => { | |
if (state.activeObject) { | |
updateObject(state.activeObject.id); | |
} | |
}} | |
> | |
Update Active Object | |
</button> | |
<button | |
onClick={() => { | |
if (state.activeObject) { | |
deleteObject(state.activeObject.id); | |
} | |
}} | |
> | |
Delete Active Object | |
</button> | |
<button | |
onClick={() => { | |
if (state.objects.length > 0) { | |
selectObject(state.objects[0].id); | |
} | |
}} | |
> | |
Select First Object | |
</button> | |
</div> | |
); | |
}; | |
export default PVEditor; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { fabric } from 'fabric'; | |
export interface CustomFabricObject extends fabric.Object { | |
id: string; | |
type: string; | |
custom: { customProp: string }; | |
attachCreateObjectEventListeners(): void; | |
detachCreateObjectEventListeners(): void; | |
} | |
type RoofAreaPolylineType = typeof fabric.Object & { | |
new (): CustomFabricObject; | |
}; | |
export const RoofAreaPolyline: RoofAreaPolylineType = fabric.util.createClass(fabric.Polyline, { | |
type: 'RoofAreaPolyline', | |
initialize(this: CustomFabricObject, options?: any) { | |
options ||= {}; | |
this.callSuper('initialize', [], options); | |
this.id = uuid(); | |
this.type = 'roof_area_polyline'; | |
this.custom = options.custom || {}; | |
this.attachCreateObjectEventListeners(); | |
}, | |
attachCreateObjectEventListeners(this: CustomFabricObject) { | |
this.canvas?.on('custom:mousedown', this.handleMouseDown); | |
this.canvas?.on('custom:mousemove', this.handleMouseMove); | |
this.canvas?.on('custom:mouseup', this.handleMouseUp); | |
}, | |
detachCreateObjectEventListeners(this: CustomFabricObject) { | |
this.canvas?.off('custom:mousedown', this.handleMouseDown); | |
this.canvas?.off('custom:mousemove', this.handleMouseMove); | |
this.canvas?.off('custom:mouseup', this.handleMouseUp); | |
}, | |
handleMouseDown(this: CustomFabricObject, event: fabric.IEvent) { | |
const pointer = event.e as MouseEvent; | |
const { x, y } = this.canvas.getPointer(pointer); | |
this.points.push({ x, y }); | |
}, | |
handleMouseMove(this: CustomFabricObject, event: fabric.IEvent) { | |
const pointer = event.e as MouseEvent; | |
const { x, y } = this.canvas.getPointer(pointer); | |
if (this.points.length > 0) { | |
this.points[this.points.length - 1] = { x, y }; | |
this.canvas?.renderAll(); | |
} | |
}, | |
handleMouseUp(this: CustomFabricObject, event: fabric.IEvent) { | |
if (this.points.length > 2) { | |
this.detachCreateObjectEventListeners(); | |
this.triggerCustomEvent('customFabricObject:completed', this.toObject(['id', 'custom'])); | |
} | |
}, | |
triggerCustomEvent(this: CustomFabricObject, eventName: string, payload: Record<string, any>) { | |
this.canvas?.fire(eventName, { target: this, ...payload }); | |
}, | |
}); | |
// TODO: Move to a shared type definition | |
declare module 'fabric' { | |
namespace fabric { | |
interface Static { | |
RoofAreaPolyline: RoofAreaPolylineType; | |
} | |
} | |
} | |
fabric.RoofAreaPolyline = RoofAreaPolyline; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useCallback } from 'react'; | |
import { fabric } from 'fabric'; | |
import { CustomFabricObject } from './RoofAreaPolyline'; | |
interface CanvasState { | |
objects: CustomFabricObject[]; | |
activeObject: CustomFabricObject | null; | |
} | |
export const useCanvas = () => { | |
const [state, setState] = useState<CanvasState>({ | |
objects: [], | |
activeObject: null, | |
}); | |
const addObject = useCallback(() => { | |
const obj = new fabric.RoofAreaPolyline() as CustomFabricObject; | |
obj.on('customFabricObject:completed', (event: fabric.IEvent) => { | |
const { target } = event; | |
setState((prevState) => ({ | |
...prevState, | |
objects: [...prevState.objects, target as CustomFabricObject], | |
})); | |
}); | |
return obj; | |
}, []); | |
const updateObject = useCallback((id: string, payload: Record<string, unknown>) => { | |
setState((prevState) => { | |
const objects = prevState.objects.map((obj) => { | |
if (obj.id === id) { | |
obj.custom = { ...obj.custom, ...payload }; | |
} | |
return obj; | |
}); | |
return { ...prevState, objects }; | |
}); | |
}, []); | |
const deleteObject = useCallback((id: string) => { | |
setState((prevState) => ({ | |
...prevState, | |
objects: prevState.objects.filter((obj) => obj.id !== id), | |
activeObject: prevState.activeObject?.id === id ? null : prevState.activeObject, | |
})); | |
}, []); | |
const selectObject = useCallback((id: string) => { | |
setState((prevState) => { | |
const object = prevState.objects.find((obj) => obj.id === id); | |
return { ...prevState, activeObject: object || null }; | |
}); | |
// TODO: notify fabric the selected as active object | |
}, []); | |
const syncActiveObject = useCallback((canvas: fabric.Canvas | null) => { | |
if (!canvas) return; | |
const activeObject = canvas.getActiveObject() as CustomFabricObject | null; | |
setState((prevState) => ({ | |
...prevState, | |
activeObject, | |
})); | |
}, []); | |
const save = useCallback(() => { | |
const dataToSave = state.objects.map((obj) => obj.toObject(['id', 'custom'])); | |
console.log('Saving to database:', JSON.stringify(dataToSave, null, 2)); | |
return Promise.resolve(true); | |
}, [state.objects]); | |
return { | |
state, | |
addObject, | |
updateObject, | |
deleteObject, | |
selectObject, | |
syncActiveObject, | |
save, | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment