Skip to content

Instantly share code, notes, and snippets.

@sjdonado
Last active November 29, 2024 12:17
Show Gist options
  • Save sjdonado/7be445f4812c578fe8b176c1eb8923b0 to your computer and use it in GitHub Desktop.
Save sjdonado/7be445f4812c578fe8b176c1eb8923b0 to your computer and use it in GitHub Desktop.
Fabric 2d editor prototype
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;
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;
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