ultimatepp/uppsrc/RichText/DiagramShape.cpp
2025-09-16 09:48:39 +02:00

519 lines
12 KiB
C++

#include "RichText.h"
namespace Upp {
Index<String> DiagramItem::LineCap = { "none", "arrow", "circle", "disc", "dim", "T",
"arrowL", "circleL", "discL", "dimL", "TL" };
Index<String> DiagramItem::Shape = { "line", "rect", "round_rect",
"ellipse", "diamond", "oval", "parallelogram",
"cylinder", "triangle",
"arrow_right", "arrow_horz", "arrow_down", "arrow_vert",
"arc",
"svgpath", "image"
};
Vector<Pointf> DiagramItem::GetConnections() const
{
Vector<Pointf> p;
if(shape > SHAPE_TRIANGLE)
return p;
if(IsLine()) {
p << pos << pos + size;
return p;
}
Rectf r = GetRect();
p << r.TopCenter() << r.BottomCenter();
if(findarg(shape, SHAPE_PARALLELOGRAM, SHAPE_TRIANGLE) < 0)
p << r.CenterLeft() << r.CenterRight();
if(shape == SHAPE_TRIANGLE)
p << r.BottomLeft() << r.BottomRight();
if(rotate) {
Xform2D rot = Rotation();
Pointf c = r.CenterPoint();
for(Pointf& x : p) {
x -= c;
x = rot.Transform(x);
x += c;
}
}
return p;
}
const Vector<double>& DiagramItem::GetDash(int i)
{
static Vector<double> dashes[10] = { { 0 }, { 1, 1 }, { 2 }, { 1, 2 }, { 2, 1 }, { 2, 1, 1, 1 }, { 3, 1 }, { 1, 3 }, { 1, 4 }, { 2, 5 } };
return dashes[clamp(i, 0, 9)];
}
void DiagramItem::Paint(Painter& w, const Diagram& diagram, dword style, const Index<Pointf> *conn) const
{
bool dark = style & DARK;
auto SC = [&](Color c) { return dark ? DarkThemeCached(c) : c; };
Color ink = SC(this->ink);
Color paper = SC(this->paper);
PaintInfo pi;
pi.darktheme = dark;
pi.zoom = Diagram::TextZoom();
RichText txt = ParseQTF(qtf);
auto DoDash = [&] {
if(dash) {
Vector<double> d = clone(GetDash(dash));
for(double& h : d)
h *= width;
w.Dash(d, 0);
}
};
RGBA sel1 = 150 * SColorHighlight();
RGBA sel2 = 150 * Gray();
double stroke = width;
auto Stroke = [&] {
if(stroke)
w.Stroke(stroke, ink);
else
if(style & GRID)
w.Stroke(0.2, sel1);
};
w.Move(0, 0).EndPath(); // this is to start a new path for every item
if(IsLine()) {
if(style) {
w.Move(pos).RelLine(size).EndPath();
w.Begin();
if((style & EDITOR) && width == 0)
w.Dash("5 1").Stroke(1, 100 * sel2);
if(style & (Display::CURSOR | Display::SELECT)) {
w.LineCap(LINECAP_ROUND).Stroke(width + 12, (style & Display::SELECT ? 30 : 200) * sel2);
double r = (width + 12) / 2 - 1;
w.Circle(pos, r).Fill(sel1);
w.Circle(pos + size, r).Fill(sel1);
}
w.End();
}
Pointf v = size;
double d = Length(v);
v = Upp::Normalize(v);
Pointf a1 = pos;
Pointf a2 = pos + size;
auto CapDef = [&](int i, double& reserve, double& reduce) {
reserve = 0;
reduce = 0;
switch(cap[i]) {
case CAP_DIM:
case CAP_ARROW:
reserve = reduce = 4 * width;
break;
case CAP_DISC:
case CAP_CIRCLE:
reserve = 1.5 * width;
break;
case CAP_DIML:
case CAP_ARROWL:
reserve = reduce = 12 * width;
break;
case CAP_DISCL:
case CAP_CIRCLEL:
reserve = 2.5 * width;
break;
}
};
bool docap[2];
double reserve, reduce;
double dd = d;
CapDef(0, reserve, reduce);
docap[0] = dd > reserve;
if(docap[0]) {
a1 += v * reduce;
dd -= reserve;
}
CapDef(1, reserve, reduce);
docap[1] = dd > reserve;
if(docap[1])
a2 -= v * reduce;
w.Move(a1).Line(a2);
DoDash();
Stroke();
Pointf o = Orthogonal(v);
auto PaintCap = [&](int k, Pointf p, Pointf a) {
Pointf oo = max(3.0, width * 2) * o;
Pointf ool = max(6.0, width * 4) * o;
switch(k) {
case CAP_NONE:
w.Circle(p, width / 2).Fill(ink);
break;
case CAP_T:
w.Move(p - 2 * oo).Line(p + 2 * oo).Stroke(1, ink);
break;
case CAP_DIM:
w.Move(p - 2 * oo).Line(p + 2 * oo).Stroke(1, ink);
case CAP_ARROW:
w.Move(p).Line(a + oo).Line(a - oo).Fill(ink);
break;
case CAP_DISC:
w.Circle(p, 1.5 * width).Fill(ink);
break;
case CAP_CIRCLE:
w.Circle(p, 1.5 * width).Fill(paper).Stroke(1, ink);
break;
case CAP_TL:
w.Move(p - 2 * ool).Line(p + 2 * ool).Stroke(1, ink);
break;
case CAP_DIML:
w.Move(p - 2 * ool).Line(p + 2 * ool).Stroke(1, ink);
case CAP_ARROWL:
w.Move(p).Line(a + ool).Line(a - ool).Fill(ink);
break;
case CAP_DISCL:
w.Circle(p, 2.5 * width).Fill(ink);
break;
case CAP_CIRCLEL:
w.Circle(p, 2.5 * width).Fill(paper).Stroke(1, ink);
break;
}
};
if(docap[0])
PaintCap(cap[0], pos, a1 + v);
if(docap[1])
PaintCap(cap[1], pos + size, a2 - v);
int cx = (int)d;
int txt_cy = txt.GetHeight(pi.zoom, cx);
w.Begin();
double angle = Bearing(size);
if(angle >= -M_PI / 2 && angle <= M_PI / 2) {
w.Translate(pos - o * (txt_cy + 10));
w.Rotate(angle);
}
else {
w.Translate(pos + size + o * (txt_cy + 10));
w.Rotate(angle + M_PI);
}
txt.Paint(w, 0, 0, cx, pi);
w.End();
}
else {
Rectf r = GetRect();
r.Normalize();
r.Deflate(width / 2);
double w1 = r.GetWidth();
double h = r.GetHeight();
double cx = r.GetWidth();
double cy = r.GetHeight();
Rectf text_rect = r.Deflated(width + 2, 0).Offseted(-r.TopLeft());
double sz = min(cx, cy);
double arrow_width = min(cx / 3, cy / 3);
double arrow_height = min(cx / 3, cy / 3);
double h2 = h / 2;
double h4 = h / 4;
double bh4 = h - h4;
double w2 = w1 / 2;
double w4 = w1 / 4;
Pointf m(w2, h2);
double hc, thc, bhc; // cylinder
w.Begin();
w.Translate(r.left, r.top);
if(rotate) {
w.Translate(w2, h2);
w.Rotate(M_2PI * rotate / 360);
w.Translate(-w2, -h2);
}
if(style & (Display::CURSOR | Display::SELECT)) {
w.RoundedRectangle(-2, -2, w1 + 4, h + 4, 5)
.Stroke(6, (style & Display::SELECT ? 30 : 200) * sel1);
w.RoundedRectangle(-1, -1, w1 + 2, h + 2, 2)
.Stroke(2, Gray());
if(style & Display::CURSOR)
w.Circle(0, h, 6).Stroke(1, Red());
}
w.Begin();
if(flip_horz) {
w.Translate(cx, 0);
w.Scale(-1, 1);
}
if(flip_vert) {
w.Translate(0, cy);
w.Scale(1, -1);
}
switch(shape) {
case SHAPE_ROUNDRECT:
w.RoundedRectangle(0, 0, w1, h, sz > 30 ? 8 : sz > 15 ? 4 : 2);
break;
case SHAPE_OVAL:
if(w1 > h) {
double ra = h2;
w.Move(ra, 0)
.Line(w1 - ra, 0)
.Arc(w1 - ra, 0 + ra, ra, -M_PI / 2, M_PI)
.Line(ra, cy)
.Arc(ra, 0 + ra, ra, M_PI / 2, M_PI);
break;
}
case SHAPE_ELLIPSE:
w.Ellipse(m.x, m.y, w2, h2);
break;
case SHAPE_DIAMOND:
w.Move(m.x, 0).Line(cx, m.y).Line(m.x, cy).Line(0, m.y).Close();
break;
case SHAPE_PARALLELOGRAM:
w.Move(cx / 6, 0).Line(cx, 0).Line(5 * cx / 6, cy).Line(0, cy).Close();
break;
case SHAPE_CYLINDER:
text_rect.top += int(w1 / 4);
hc = h / 6;
thc = 0 + hc;
bhc = cy - hc;
w.Move(0, thc)
.Arc(0 + w2, thc, w2, hc, M_PI, M_PI)
.Line(cx, bhc)
.Arc(0 + w2, bhc, w2, hc, 0, M_PI)
.Line(0, bhc);
break;
case SHAPE_TRIANGLE: {
text_rect.left += w4;
text_rect.right -= w4;
text_rect.top += int(cx / 3);
w.Move(w2, 0).Line(cx, cy).Line(0, cy).Close();
}
break;
case SHAPE_ARROWRIGHT:
{
double a = cx - arrow_width;
text_rect.right -= int(arrow_width / 3);
w.Move(cx, cy / 2)
.Line(a, 0)
.Line(a, h4)
.Line(0, h4)
.Line(0, bh4)
.Line(a, bh4)
.Line(a, cy)
.Close();
}
break;
case SHAPE_ARROWHORZ:
{
double a1 = 0 + arrow_width;
text_rect.left += int(arrow_width / 3);
double a2 = cx - arrow_width;
text_rect.right -= int(arrow_width / 3);
w.Move(0, h2)
.Line(a1, 0)
.Line(a1, h4)
.Line(a2, h4)
.Line(a2, 0)
.Line(cx, h2)
.Line(a2, cy)
.Line(a2, bh4)
.Line(a1, bh4)
.Line(a1, cy)
.Close();
}
break;
case SHAPE_ARROWDOWN: {
double a = cy - arrow_height;
text_rect.left += w4;
text_rect.right -= w4;
text_rect.bottom -= 3 * arrow_height / 4;
w.Move(0 + cx / 2, cy)
.Line(cx, a)
.Line(cx - w4, a)
.Line(cx - w4, 0)
.Line(w4, 0)
.Line(w4, a)
.Line(0, a)
.Close();
}
break;
case SHAPE_ARROWVERT: {
double a1 = 0 + arrow_height;
double a2 = cy - arrow_height;
text_rect.left += w4;
text_rect.right -= w4;
w.Move(cx / 2, 0)
.Line(cx, a1)
.Line(cx - w4, a1)
.Line(cx - w4, a2)
.Line(cx, a2)
.Line(cx / 2, cy)
.Line(0, a2)
.Line(w4, a2)
.Line(w4, a1)
.Line(0, a1)
.Close();
}
break;
case SHAPE_ARC:
if(w2 > 0) {
double a = (sqr(w2) - sqr(cy)) / (2 * cy);
double f = atan(w2 / a);
if(a >= 0)
w.Move(0, cy).Arc(w2, a + cy, a + cy, -M_PI / 2 - f, 2 * f);
else
w.Move(0, cy).Arc(w2, cy, w2, cy, M_PI, M_PI);
}
break;
case SHAPE_SVGPATH: {
Rectf bb = diagram.GetBlobSvgPathBoundingBox(blob_id);
Sizef size = bb.GetSize();
if(size.cx > 0 && size.cy > 0) {
w.Scale(w1 / size.cx, h / size.cy);
stroke *= min(size.cx / w1, size.cy / h);
w.Translate(-bb.TopLeft());
w.Path(diagram.GetBlob(blob_id));
}
}
break;
case SHAPE_IMAGE: {
if(style & FAST) {
w.Rectangle(0, 0, cx, cy).Fill(Gray());
break;
}
String s = diagram.GetBlob(blob_id);
if(s.GetCount()) {
if(IsSVG(s)) {
Rectf f = GetSVGBoundingBox(s);
w.Scale(cx / f.GetWidth(), cy / f.GetHeight());
w.Translate(-f.left, -f.top);
RenderSVG(w, s, Event<String, String&>(), paper);
}
else {
StringStream ss(s);
Value v = MakeValue(
[&] {
return blob_id;
},
[&](Value& v) {
v = Null;
One<StreamRaster> r = StreamRaster::OpenAny(ss);
if(r) {
Image m = r->GetImage();
v = m;
return int(m.GetLength() * sizeof(RGBA));
}
return 0;
}
);
if(v.Is<Image>()) {
Image img = v;
w.Rectangle(0, 0, cx, cy).Fill(img, Xform2D::Scale(cx / img.GetWidth(), cy / img.GetHeight()));
}
}
}
}
break;
default:
w.Rectangle(0, 0, cx, cy);
break;
}
if(shape != SHAPE_IMAGE) {
DoDash();
if(shape != SHAPE_ARC)
w.Fill(paper);
Stroke();
}
switch(shape) {
case SHAPE_CYLINDER:
w.Move(0, thc)
.Arc(0 + w2, thc, w2, hc, M_PI, -M_PI);
DoDash();
Stroke();
break;
}
w.End();
Rectf tr2 = text_rect;
if(flip_horz) {
text_rect.right = cx - tr2.left;
text_rect.left = cx - tr2.right;
}
if(flip_vert) {
text_rect.top = cy - tr2.bottom;
text_rect.bottom = cy - tr2.top;
}
Rect tr = text_rect;
int txt_cy = txt.GetHeight(pi.zoom, tr.GetWidth());
txt.Paint(w, tr.left, tr.top + (tr.GetHeight() - txt_cy) / 2, tr.GetWidth(), pi);
w.End();
if((style & GRID))
for(Pointf p : GetConnections()) {
w.Circle(p, 5);
if(conn && conn->Find(p) >= 0)
w.Fill(128 * SYellow());
w.Stroke(1, 190 * SColorHighlight());
}
}
}
Sizef DiagramItem::GetStdSize(const Diagram& diagram) const
{
switch(shape) {
case SHAPE_SVGPATH:
return diagram.GetBlobSvgPathBoundingBox(blob_id).GetSize();
case SHAPE_IMAGE: {
String s = diagram.GetBlob(blob_id);
if(s.GetCount()) {
if(IsSVG(s))
return GetSVGBoundingBox(s).GetSize();
else {
StringStream ss(s);
Value v = MakeValue(
[&] {
return blob_id;
},
[&](Value& v) {
v = Null;
One<StreamRaster> r = StreamRaster::OpenAny(ss);
if(r) {
v = r->GetSize();
return 32;
}
return 0;
}
);
if(v.Is<Size>())
return v.To<Size>();
}
}
}
}
if(shape == SHAPE_CYLINDER)
return Size(96, 128);
if(findarg(shape, SHAPE_CYLINDER, SHAPE_ARROWDOWN, SHAPE_ARROWVERT) >= 0)
return Size(64, 128);
return Size(128, 64);
}
}