Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/Handlers/ToolsHandler/assignUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Font,
Diamond,
Polygon,
Cylinder,
Arrow,
Line,
Eraser,
Expand All @@ -16,6 +17,7 @@ const toolFactory = {
[TOOL_CONSTANTS.FONT]: new Font(),
[TOOL_CONSTANTS.DIAMOND]: new Diamond(),
[TOOL_CONSTANTS.POLYGON]: new Polygon(),
[TOOL_CONSTANTS.CYLINDER]: new Cylinder(),
[TOOL_CONSTANTS.ARROW]: new Arrow(),
[TOOL_CONSTANTS.LINE]: new Line(),
[TOOL_CONSTANTS.ERASER]: new Eraser(),
Expand Down
102 changes: 102 additions & 0 deletions src/Handlers/ToolsHandler/tools/cylinder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { fabric } from "fabric";
import { Tool } from "../toolGeneric";
import { resolveToolStyle } from "../toolStyle";

// Default size for a plain click (no drag), like the other shapes.
const DEFAULT_W = 80;
const DEFAULT_H = 100;

// SVG path for a database cylinder in a w x h box (top-left origin). Built from
// cubic beziers (not SVG arcs) so the rim curve directions are unambiguous and
// fabric fills it cleanly. `body` is the closed silhouette — straight sides, a
// back top rim (bulges up) and a front bottom rim (bulges down), so its fill
// covers the whole cylinder incl. the top ellipse band. `lid` is the top front
// rim, stroked on top so the lid line reads. k = ellipse bezier constant.
const cylinderPath = (w, h) => {
const rx = w / 2;
const ry = Math.max(1, Math.min(h * 0.16, w * 0.35, h / 2 - 0.5));
const k = 0.5523;
const kx = rx * k;
const ky = ry * k;
const topC = ry; // centre line of the top ellipse
const botC = h - ry; // centre line of the bottom ellipse
const body =
`M 0 ${topC} L 0 ${botC} ` +
`C 0 ${botC + ky} ${rx - kx} ${botC + ry} ${rx} ${botC + ry} ` +
`C ${rx + kx} ${botC + ry} ${w} ${botC + ky} ${w} ${botC} ` +
`L ${w} ${topC} ` +
`C ${w} ${topC - ky} ${rx + kx} ${topC - ry} ${rx} ${topC - ry} ` +
`C ${rx - kx} ${topC - ry} 0 ${topC - ky} 0 ${topC} Z`;
const lid =
`M 0 ${topC} ` +
`C 0 ${topC + ky} ${rx - kx} ${topC + ry} ${rx} ${topC + ry} ` +
`C ${rx + kx} ${topC + ry} ${w} ${topC + ky} ${w} ${topC}`;
return `${body} ${lid}`;
};

export class Cylinder extends Tool {
constructor() {
super();
this.origX = null;
this.origY = null;
this.pointer = null;
this.cylinder = null;
}

// Rewrite the path for the given box and pin its top-left, so the shape grows
// from the drag start (like the rectangle) without accumulating scale. The
// rim arcs bulge to exactly the box edges, but fabric's arc bbox misses the
// bottom bulge — set width/height/pathOffset to the true box so selection and
// binding match what's drawn.
applyBox(left, top, width, height) {
this.cylinder._setPath(fabric.util.parsePath(cylinderPath(width, height)));
this.cylinder.set({
width,
height,
pathOffset: { x: width / 2, y: height / 2 },
left,
top,
});
this.cylinder.setCoords();
}

create(canvas, event) {
this.pointer = canvas.getPointer(event.e);
this.origX = this.pointer.x;
this.origY = this.pointer.y;
this.cylinder = new fabric.Path(cylinderPath(1, 1), {
left: this.origX,
top: this.origY,
originX: "left",
originY: "top",
...resolveToolStyle(canvas),
objectCaching: false, // recompute the bounding box as it is dragged
selectable: true,
});
canvas.add(this.cylinder);
}

draw(canvas, event) {
if (!this.cylinder) {
return;
}
this.pointer = canvas.getPointer(event.e);
// min() (not abs) so dragging up/left works in negative scene coords too.
const left = Math.min(this.origX, this.pointer.x);
const top = Math.min(this.origY, this.pointer.y);
const width = Math.max(Math.abs(this.origX - this.pointer.x), 1);
const height = Math.max(Math.abs(this.origY - this.pointer.y), 1);
this.applyBox(left, top, width, height);
}

done() {
if (!this.cylinder) {
return;
}
// A plain click leaves a near-zero shape — drop a default-sized one instead.
if (this.cylinder.width < 5 || this.cylinder.height < 5) {
this.applyBox(this.origX, this.origY, DEFAULT_W, DEFAULT_H);
}
this.cylinder.setCoords();
}
}
52 changes: 52 additions & 0 deletions src/Handlers/ToolsHandler/tools/cylinder.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Cylinder } from "./cylinder";
import { isBindable } from "../../../utils/binding";

// Minimal canvas stub: the tool only needs getPointer + add.
const mockCanvas = () => {
const objects = [];
return {
objects,
add: (o) => objects.push(o),
getPointer: (e) => ({ x: e.clientX, y: e.clientY }),
};
};
const evt = (x, y) => ({ e: { clientX: x, clientY: y } });

const drawCylinder = (from, to) => {
const canvas = mockCanvas();
const tool = new Cylinder();
tool.create(canvas, evt(from.x, from.y));
tool.draw(canvas, evt(to.x, to.y));
tool.done(canvas);
return { canvas, shape: canvas.objects[0] };
};

describe("Cylinder tool", () => {
test("drag-to-size builds a path sized to the drag box", () => {
const { shape } = drawCylinder({ x: 100, y: 100 }, { x: 260, y: 300 });
expect(shape.type).toBe("path");
expect(Math.round(shape.width)).toBe(160);
expect(Math.round(shape.height)).toBe(200);
expect(Math.round(shape.left)).toBe(100);
expect(Math.round(shape.top)).toBe(100);
});

test("dragging up/left still sizes from the top-left corner", () => {
const { shape } = drawCylinder({ x: 300, y: 300 }, { x: 200, y: 220 });
expect(Math.round(shape.left)).toBe(200);
expect(Math.round(shape.top)).toBe(220);
expect(Math.round(shape.width)).toBe(100);
expect(Math.round(shape.height)).toBe(80);
});

test("a plain click (no drag) drops a default-sized cylinder", () => {
const { shape } = drawCylinder({ x: 50, y: 50 }, { x: 51, y: 51 });
expect(shape.width).toBeGreaterThan(10);
expect(shape.height).toBeGreaterThan(10);
});

test("the cylinder is a bindable shape (arrows can attach to it)", () => {
const { shape } = drawCylinder({ x: 0, y: 0 }, { x: 120, y: 120 });
expect(isBindable(shape)).toBe(true);
});
});
1 change: 1 addition & 0 deletions src/Handlers/ToolsHandler/tools/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { Circle } from "./circle";
export { Font } from "./font";
export { Diamond } from "./diamond";
export { Polygon } from "./polygon";
export { Cylinder } from "./cylinder";
export { Arrow } from "./arrow";
export { Line } from "./line";
export { Eraser } from "./eraser";
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const STYLEABLE_TOOLS = new Set([
TOOL_CONSTANTS.CIRCLE,
TOOL_CONSTANTS.DIAMOND,
TOOL_CONSTANTS.POLYGON,
TOOL_CONSTANTS.CYLINDER,
TOOL_CONSTANTS.LINE,
TOOL_CONSTANTS.ARROW,
TOOL_CONSTANTS.FONT,
Expand Down
2 changes: 2 additions & 0 deletions src/constants/IconTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Circle,
Diamond,
Hexagon,
Cylinder,
Minus,
ArrowRight,
Type,
Expand All @@ -22,6 +23,7 @@ export const iconToolsMaps = [
{ icon: Circle, title: "Circle", id: TOOL_CONSTANTS.CIRCLE },
{ icon: Diamond, title: "Diamond", id: TOOL_CONSTANTS.DIAMOND },
{ icon: Hexagon, title: "Polygon", id: TOOL_CONSTANTS.POLYGON },
{ icon: Cylinder, title: "Cylinder", id: TOOL_CONSTANTS.CYLINDER },
{ icon: Minus, title: "Line", id: TOOL_CONSTANTS.LINE },
{ icon: ArrowRight, title: "Arrow", id: TOOL_CONSTANTS.ARROW },
{ icon: Type, title: "Text", id: TOOL_CONSTANTS.FONT },
Expand Down
5 changes: 5 additions & 0 deletions src/constants/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const TOOL_CONSTANTS = {
BACKGROUND_COLOR: "bgColors",
DIAMOND: "diamond",
POLYGON: "polygon",
CYLINDER: "cylinder",
ERASER: "eraser",
ICONS: "icons",
};
Expand Down Expand Up @@ -59,6 +60,10 @@ export const TOOL_FUNCTIONS = {
createOnClick: true,
onMove: true,
},
[TOOL_CONSTANTS.CYLINDER]: {
createOnClick: true,
onMove: true,
},
[TOOL_CONSTANTS.ERASER]: {
createOnClick: true,
onMove: true,
Expand Down
Loading