Compare commits
2 Commits
b1177265ea
...
d6d41388b3
| Author | SHA1 | Date | |
|---|---|---|---|
| d6d41388b3 | |||
| 4bf92356af |
@@ -15,6 +15,8 @@ pub const Data = union(enum) {
|
|||||||
|
|
||||||
size: Size_f,
|
size: Size_f,
|
||||||
radii: Radii_f,
|
radii: Radii_f,
|
||||||
|
/// Процент дуги эллипса: 100 — полный эллипс, 50 — полуэллипс (0..100).
|
||||||
|
arc_percent: f32,
|
||||||
end_point: Point2_f,
|
end_point: Point2_f,
|
||||||
|
|
||||||
points: std.ArrayList(Point2_f),
|
points: std.ArrayList(Point2_f),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const shape_mod = @import("shape.zig");
|
|||||||
/// Свойства фигуры по умолчанию.
|
/// Свойства фигуры по умолчанию.
|
||||||
pub const default_shape_properties = [_]Property{
|
pub const default_shape_properties = [_]Property{
|
||||||
.{ .data = .{ .radii = .{ .x = 50, .y = 50 } } },
|
.{ .data = .{ .radii = .{ .x = 50, .y = 50 } } },
|
||||||
|
.{ .data = .{ .arc_percent = 100.0 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Теги обязательных свойств.
|
/// Теги обязательных свойств.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ fn drawObject(
|
|||||||
|
|
||||||
switch (obj.shape) {
|
switch (obj.shape) {
|
||||||
.line => line.draw(ctx, obj),
|
.line => line.draw(ctx, obj),
|
||||||
.ellipse => ellipse.draw(ctx, obj),
|
.ellipse => try ellipse.draw(ctx, obj, allocator),
|
||||||
.broken => try broken.draw(ctx, obj, allocator),
|
.broken => try broken.draw(ctx, obj, allocator),
|
||||||
.arc => arc.draw(ctx, obj),
|
.arc => arc.draw(ctx, obj),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const std_math = std.math;
|
||||||
const Document = @import("../../models/Document.zig");
|
const Document = @import("../../models/Document.zig");
|
||||||
const pipeline = @import("pipeline.zig");
|
const pipeline = @import("pipeline.zig");
|
||||||
const DrawContext = pipeline.DrawContext;
|
const DrawContext = pipeline.DrawContext;
|
||||||
const Color = @import("dvui").Color;
|
const Color = @import("dvui").Color;
|
||||||
|
const basic_models = @import("../../models/basic_models.zig");
|
||||||
|
const Point2_f = basic_models.Point2_f;
|
||||||
|
|
||||||
const Object = Document.Object;
|
const Object = Document.Object;
|
||||||
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
|
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
|
||||||
const default_thickness: f32 = 2.0;
|
const default_thickness: f32 = 2.0;
|
||||||
|
|
||||||
/// Эллипс с центром (0,0) и полуосями radii (обводка с учётом thickness).
|
/// Эллипс с центром (0,0) и полуосями radii. Обводка — полоса расстояния до контура (чёткая линия, не круги).
|
||||||
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
|
/// arc_percent: 100 — полный эллипс, иначе одна дуга; обход в коде от (0,ry) по квадрантам (визуально может казаться от низа против часовой из‑за экранной Y).
|
||||||
|
/// Отрисовка в отдельный буфер и один composite, чтобы при alpha<255 пиксели не накладывались несколько раз.
|
||||||
|
pub fn draw(ctx: *DrawContext, obj: *const Object, allocator: std.mem.Allocator) !void {
|
||||||
const r_prop = obj.getProperty(.radii) orelse return;
|
const r_prop = obj.getProperty(.radii) orelse return;
|
||||||
const rx = r_prop.radii.x;
|
const rx = r_prop.radii.x;
|
||||||
const ry = r_prop.radii.y;
|
const ry = r_prop.radii.y;
|
||||||
if (rx <= 0 or ry <= 0) return;
|
if (rx <= 0 or ry <= 0) return;
|
||||||
|
|
||||||
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke;
|
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke;
|
||||||
const thickness = if (obj.getProperty(.thickness)) |t| t.thickness else default_thickness;
|
const thickness = if (obj.getProperty(.thickness)) |t| t.thickness else default_thickness;
|
||||||
|
const arc_percent = std_math.clamp(if (obj.getProperty(.arc_percent)) |p| p.arc_percent else 100.0, 0.0, 100.0);
|
||||||
|
|
||||||
// Полуширина обводки в нормализованных единицах (d = (x/rx)² + (y/ry)², граница при d=1).
|
const t = &ctx.transform;
|
||||||
const min_r = @min(rx, ry);
|
const min_r = @min(rx, ry);
|
||||||
const half_norm = thickness / (2.0 * min_r);
|
const half_norm = thickness / (2.0 * min_r);
|
||||||
const inner = @max(0.0, 1.0 - half_norm);
|
const inner = @max(0.0, 1.0 - half_norm);
|
||||||
@@ -25,19 +32,18 @@ pub fn draw(ctx: *DrawContext, obj: *const Object) void {
|
|||||||
const d_inner_sq = inner * inner;
|
const d_inner_sq = inner * inner;
|
||||||
const d_outer_sq = outer * outer;
|
const d_outer_sq = outer * outer;
|
||||||
|
|
||||||
const corners = [_]struct { x: f32, y: f32 }{
|
const margin = 1.0 + half_norm;
|
||||||
.{ .x = -rx, .y = -ry },
|
const corners = [_]Point2_f{
|
||||||
.{ .x = rx, .y = -ry },
|
.{ .x = -rx * margin, .y = -ry * margin },
|
||||||
.{ .x = rx, .y = ry },
|
.{ .x = rx * margin, .y = -ry * margin },
|
||||||
.{ .x = -rx, .y = ry },
|
.{ .x = rx * margin, .y = ry * margin },
|
||||||
|
.{ .x = -rx * margin, .y = ry * margin },
|
||||||
};
|
};
|
||||||
const w0 = ctx.localToWorld(corners[0].x, corners[0].y);
|
var min_bx: f32 = std_math.inf(f32);
|
||||||
const b0 = ctx.worldToBufferF(w0.x, w0.y);
|
var min_by: f32 = std_math.inf(f32);
|
||||||
var min_bx: f32 = b0.x;
|
var max_bx: f32 = -std_math.inf(f32);
|
||||||
var min_by: f32 = b0.y;
|
var max_by: f32 = -std_math.inf(f32);
|
||||||
var max_bx: f32 = b0.x;
|
for (corners) |c| {
|
||||||
var max_by: f32 = b0.y;
|
|
||||||
for (corners[1..]) |c| {
|
|
||||||
const w = ctx.localToWorld(c.x, c.y);
|
const w = ctx.localToWorld(c.x, c.y);
|
||||||
const b = ctx.worldToBufferF(w.x, w.y);
|
const b = ctx.worldToBufferF(w.x, w.y);
|
||||||
min_bx = @min(min_bx, b.x);
|
min_bx = @min(min_bx, b.x);
|
||||||
@@ -47,50 +53,43 @@ pub fn draw(ctx: *DrawContext, obj: *const Object) void {
|
|||||||
}
|
}
|
||||||
const buf_w: i32 = @intCast(ctx.buf_width);
|
const buf_w: i32 = @intCast(ctx.buf_width);
|
||||||
const buf_h: i32 = @intCast(ctx.buf_height);
|
const buf_h: i32 = @intCast(ctx.buf_height);
|
||||||
const x0: i32 = @max(0, @as(i32, @intFromFloat(std.math.floor(min_bx))));
|
const x0: i32 = @max(0, @as(i32, @intFromFloat(std_math.floor(min_bx))));
|
||||||
const y0: i32 = @max(0, @as(i32, @intFromFloat(std.math.floor(min_by))));
|
const y0: i32 = @max(0, @as(i32, @intFromFloat(std_math.floor(min_by))));
|
||||||
const x1: i32 = @min(buf_w, @as(i32, @intFromFloat(std.math.ceil(max_bx))) + 1);
|
const x1: i32 = @min(buf_w, @as(i32, @intFromFloat(std_math.ceil(max_bx))) + 1);
|
||||||
const y1: i32 = @min(buf_h, @as(i32, @intFromFloat(std.math.ceil(max_by))) + 1);
|
const y1: i32 = @min(buf_h, @as(i32, @intFromFloat(std_math.ceil(max_by))) + 1);
|
||||||
|
|
||||||
|
const buffer = try allocator.alloc(Color.PMA, ctx.buf_width * ctx.buf_height);
|
||||||
|
@memset(buffer, .{ .r = 0, .g = 0, .b = 0, .a = 0 });
|
||||||
|
defer allocator.free(buffer);
|
||||||
|
|
||||||
|
var stroke_ctx = ctx.*;
|
||||||
|
stroke_ctx.pixels = buffer;
|
||||||
|
stroke_ctx.replace_mode = true;
|
||||||
|
|
||||||
// Один раз считаем аффин buffer -> local, чтобы в цикле не вызывать cos/sin и лишние функции.
|
|
||||||
const t = &ctx.transform;
|
|
||||||
const ctx_sx = if (ctx.scale_x != 0) ctx.scale_x else 1.0;
|
|
||||||
const ctx_sy = if (ctx.scale_y != 0) ctx.scale_y else 1.0;
|
|
||||||
const inv_ctx_sx = 1.0 / ctx_sx;
|
|
||||||
const inv_ctx_sy = 1.0 / ctx_sy;
|
|
||||||
const vx = @as(f32, @floatFromInt(ctx.visible_rect.x));
|
|
||||||
const vy = @as(f32, @floatFromInt(ctx.visible_rect.y));
|
|
||||||
const t_sx = if (t.scale.scale_x != 0) t.scale.scale_x else 1.0;
|
|
||||||
const t_sy = if (t.scale.scale_y != 0) t.scale.scale_y else 1.0;
|
|
||||||
const ca = std.math.cos(-t.angle);
|
|
||||||
const sa = std.math.sin(-t.angle);
|
|
||||||
const dx_off = vx * inv_ctx_sx - t.position.x;
|
|
||||||
const dy_off = vy * inv_ctx_sy - t.position.y;
|
|
||||||
const loc_x_off = (dx_off * ca - dy_off * sa) / t_sx;
|
|
||||||
const loc_y_off = (dx_off * sa + dy_off * ca) / t_sy;
|
|
||||||
const m00 = inv_ctx_sx * ca / t_sx;
|
|
||||||
const m01 = -inv_ctx_sy * sa / t_sx;
|
|
||||||
const m10 = inv_ctx_sx * sa / t_sy;
|
|
||||||
const m11 = inv_ctx_sy * ca / t_sy;
|
|
||||||
const inv_rx = 1.0 / rx;
|
const inv_rx = 1.0 / rx;
|
||||||
const inv_ry = 1.0 / ry;
|
const inv_ry = 1.0 / ry;
|
||||||
|
const arc_len = 2.0 * std_math.pi * arc_percent / 100.0;
|
||||||
|
|
||||||
var by: i32 = y0;
|
var by: i32 = y0;
|
||||||
while (by < y1) : (by += 1) {
|
while (by < y1) : (by += 1) {
|
||||||
const buf_y = @as(f32, @floatFromInt(by)) + 0.5;
|
const buf_y = @as(f32, @floatFromInt(by)) + 0.5;
|
||||||
const row_loc_x_off = buf_y * m01 + loc_x_off;
|
|
||||||
const row_loc_y_off = buf_y * m11 + loc_y_off;
|
|
||||||
var bx: i32 = x0;
|
var bx: i32 = x0;
|
||||||
while (bx < x1) : (bx += 1) {
|
while (bx < x1) : (bx += 1) {
|
||||||
const buf_x = @as(f32, @floatFromInt(bx)) + 0.5;
|
const buf_x = @as(f32, @floatFromInt(bx)) + 0.5;
|
||||||
const loc_x = buf_x * m00 + row_loc_x_off;
|
const w = stroke_ctx.bufferToWorld(buf_x, buf_y);
|
||||||
const loc_y = buf_x * m10 + row_loc_y_off;
|
const loc = stroke_ctx.worldToLocal(w.x, w.y);
|
||||||
const nx = loc_x * inv_rx;
|
const nx = loc.x * inv_rx;
|
||||||
const ny = loc_y * inv_ry;
|
const ny = loc.y * inv_ry;
|
||||||
const d = nx * nx + ny * ny;
|
const d = nx * nx + ny * ny;
|
||||||
if (d >= d_inner_sq and d <= d_outer_sq) {
|
if (d < d_inner_sq or d > d_outer_sq) continue;
|
||||||
ctx.blendPixelAtBuffer(bx, by, stroke);
|
if (arc_percent < 100.0) {
|
||||||
}
|
var diff = std_math.pi / 2.0 - std_math.atan2(ny, nx);
|
||||||
|
if (diff < 0) diff += 2.0 * std_math.pi;
|
||||||
|
if (diff > arc_len) continue;
|
||||||
|
}
|
||||||
|
stroke_ctx.blendPixelAtBuffer(bx, by, stroke);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.compositeDrawerContext(&stroke_ctx, t.opacity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -532,6 +532,13 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope
|
|||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
.arc_percent => |pct| {
|
||||||
|
var next = pct;
|
||||||
|
if (dvui.sliderEntry(@src(), "{d:0.0}%", .{ .value = &next, .min = 0.0, .max = 100.0, .interval = 1.0 }, .{ .expand = .horizontal })) {
|
||||||
|
obj.setProperty(canvas.allocator, .{ .data = .{ .arc_percent = next } }) catch {};
|
||||||
|
canvas.requestRedraw();
|
||||||
|
}
|
||||||
|
},
|
||||||
.end_point => |pt| {
|
.end_point => |pt| {
|
||||||
var next = pt;
|
var next = pt;
|
||||||
var changed = false;
|
var changed = false;
|
||||||
@@ -745,6 +752,7 @@ fn propertyLabel(tag: std.meta.Tag(PropertyData)) []const u8 {
|
|||||||
.locked => "Locked",
|
.locked => "Locked",
|
||||||
.size => "Size",
|
.size => "Size",
|
||||||
.radii => "Radii",
|
.radii => "Radii",
|
||||||
|
.arc_percent => "Arc %",
|
||||||
.end_point => "End point",
|
.end_point => "End point",
|
||||||
.points => "Points",
|
.points => "Points",
|
||||||
.fill_rgba => "Fill color",
|
.fill_rgba => "Fill color",
|
||||||
|
|||||||
Reference in New Issue
Block a user