Compare commits
2 Commits
4f386c981c
...
e5dd455d14
| Author | SHA1 | Date | |
|---|---|---|---|
| e5dd455d14 | |||
| ef768e9fe7 |
@@ -3,7 +3,9 @@ const builtin = @import("builtin");
|
|||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const Document = @import("models/Document.zig");
|
const Document = @import("models/Document.zig");
|
||||||
const RenderEngine = @import("render/RenderEngine.zig").RenderEngine;
|
const RenderEngine = @import("render/RenderEngine.zig").RenderEngine;
|
||||||
const ImageRect = @import("models/basic_models.zig").ImageRect;
|
const basic_models = @import("models/basic_models.zig");
|
||||||
|
const ImageRect = basic_models.ImageRect;
|
||||||
|
const ImageSize = basic_models.ImageSize;
|
||||||
const Point2 = @import("models/basic_models.zig").Point2;
|
const Point2 = @import("models/basic_models.zig").Point2;
|
||||||
const Color = dvui.Color;
|
const Color = dvui.Color;
|
||||||
|
|
||||||
@@ -25,6 +27,8 @@ _zoom: f32 = 1,
|
|||||||
_redraw_pending: bool = false,
|
_redraw_pending: bool = false,
|
||||||
_last_redraw_time_ms: i64 = 0,
|
_last_redraw_time_ms: i64 = 0,
|
||||||
cursor_document_point: ?Point2 = null,
|
cursor_document_point: ?Point2 = null,
|
||||||
|
/// true — рисовать документ (render), false — пример (gradient/squares).
|
||||||
|
draw_document: bool = true,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas {
|
pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas {
|
||||||
return .{
|
return .{
|
||||||
@@ -41,7 +45,7 @@ pub fn deinit(self: *Canvas) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redrawExample(self: *Canvas) !void {
|
fn redraw(self: *Canvas) !void {
|
||||||
const full = self.getZoomedImageSize();
|
const full = self.getZoomedImageSize();
|
||||||
|
|
||||||
const vis: ImageRect = self._visible_rect orelse ImageRect{ .x = 0, .y = 0, .w = 0, .h = 0 };
|
const vis: ImageRect = self._visible_rect orelse ImageRect{ .x = 0, .y = 0, .w = 0, .h = 0 };
|
||||||
@@ -54,7 +58,11 @@ pub fn redrawExample(self: *Canvas) !void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const new_texture = self.render_engine.example(.{ .w = full.w, .h = full.h }, vis) catch null;
|
const canvas_size: ImageSize = .{ .w = full.w, .h = full.h };
|
||||||
|
const new_texture = if (self.draw_document)
|
||||||
|
self.render_engine.render(self.document, canvas_size, vis) catch null
|
||||||
|
else
|
||||||
|
self.render_engine.example(canvas_size, vis) catch null;
|
||||||
|
|
||||||
if (new_texture) |tex| {
|
if (new_texture) |tex| {
|
||||||
if (self.texture) |old_tex| {
|
if (self.texture) |old_tex| {
|
||||||
@@ -68,7 +76,7 @@ pub fn redrawExample(self: *Canvas) !void {
|
|||||||
|
|
||||||
pub fn exampleReset(self: *Canvas) !void {
|
pub fn exampleReset(self: *Canvas) !void {
|
||||||
self.render_engine.exampleReset();
|
self.render_engine.exampleReset();
|
||||||
try self.redrawExample();
|
try self.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setZoom(self: *Canvas, value: f32) void {
|
pub fn setZoom(self: *Canvas, value: f32) void {
|
||||||
@@ -88,14 +96,14 @@ pub fn processPendingRedraw(self: *Canvas) !void {
|
|||||||
if (!self._redraw_pending) return;
|
if (!self._redraw_pending) return;
|
||||||
if (self.redraw_throttle_ms == 0) {
|
if (self.redraw_throttle_ms == 0) {
|
||||||
self._redraw_pending = false;
|
self._redraw_pending = false;
|
||||||
try self.redrawExample();
|
try self.redraw();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const now_ms = std.time.milliTimestamp();
|
const now_ms = std.time.milliTimestamp();
|
||||||
const elapsed: i64 = if (self._last_redraw_time_ms == 0) self.redraw_throttle_ms else now_ms - self._last_redraw_time_ms;
|
const elapsed: i64 = if (self._last_redraw_time_ms == 0) self.redraw_throttle_ms else now_ms - self._last_redraw_time_ms;
|
||||||
if (elapsed < @as(i64, @intCast(self.redraw_throttle_ms))) return;
|
if (elapsed < @as(i64, @intCast(self.redraw_throttle_ms))) return;
|
||||||
self._redraw_pending = false;
|
self._redraw_pending = false;
|
||||||
try self.redrawExample();
|
try self.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getZoomedImageSize(self: Canvas) ImageRect {
|
pub fn getZoomedImageSize(self: Canvas) ImageRect {
|
||||||
@@ -134,9 +142,6 @@ pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset:
|
|||||||
changed |= next.x != vis.x or next.y != vis.y or next.w != vis.w or next.h != vis.h;
|
changed |= next.x != vis.x or next.y != vis.y or next.w != vis.w or next.h != vis.h;
|
||||||
}
|
}
|
||||||
self._visible_rect = next;
|
self._visible_rect = next;
|
||||||
if (changed) {
|
|
||||||
std.debug.print("Visible Image Rect: {{ x: {}, y: {}, w: {}, h: {} }}\n", .{ next.x, next.y, next.w, next.h });
|
|
||||||
}
|
|
||||||
if (changed or self.texture == null) {
|
if (changed or self.texture == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ const std = @import("std");
|
|||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const RenderEngine = @import("RenderEngine.zig").RenderEngine;
|
const RenderEngine = @import("RenderEngine.zig").RenderEngine;
|
||||||
|
const Document = @import("../models/Document.zig");
|
||||||
const basic_models = @import("../models/basic_models.zig");
|
const basic_models = @import("../models/basic_models.zig");
|
||||||
|
const cpu_draw = @import("cpu/draw.zig");
|
||||||
const ImageSize = basic_models.ImageSize;
|
const ImageSize = basic_models.ImageSize;
|
||||||
const ImageRect = basic_models.ImageRect;
|
const ImageRect = basic_models.ImageRect;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
@@ -171,3 +173,16 @@ pub fn example(self: CpuRenderEngine, canvas_size: ImageSize, visible_rect: Imag
|
|||||||
pub fn renderEngine(self: *CpuRenderEngine) RenderEngine {
|
pub fn renderEngine(self: *CpuRenderEngine) RenderEngine {
|
||||||
return .{ .cpu = self };
|
return .{ .cpu = self };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Растеризует документ: фон + рекурсивная отрисовка фигур через конвейер (трансформ, прозрачность, наложение).
|
||||||
|
pub fn renderDocument(self: *CpuRenderEngine, document: *const Document, canvas_size: ImageSize, visible_rect: ImageRect) !?dvui.Texture {
|
||||||
|
const width = visible_rect.w;
|
||||||
|
const height = visible_rect.h;
|
||||||
|
const pixels = try self._allocator.alloc(Color.PMA, @as(usize, width) * height);
|
||||||
|
defer self._allocator.free(pixels);
|
||||||
|
|
||||||
|
for (pixels) |*p| p.* = .{ .r = 255, .g = 255, .b = 255, .a = 255 };
|
||||||
|
cpu_draw.drawDocument(pixels, width, height, visible_rect, document, canvas_size);
|
||||||
|
|
||||||
|
return try dvui.textureCreate(pixels, width, height, .nearest);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const CpuRenderEngine = @import("CpuRenderEngine.zig");
|
const CpuRenderEngine = @import("CpuRenderEngine.zig");
|
||||||
|
const Document = @import("../models/Document.zig");
|
||||||
const basic_models = @import("../models/basic_models.zig");
|
const basic_models = @import("../models/basic_models.zig");
|
||||||
|
|
||||||
pub const RenderEngine = union(enum) {
|
pub const RenderEngine = union(enum) {
|
||||||
@@ -16,4 +17,11 @@ pub const RenderEngine = union(enum) {
|
|||||||
.cpu => |cpu_r| cpu_r.example(canvas_size, visible_rect),
|
.cpu => |cpu_r| cpu_r.example(canvas_size, visible_rect),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Растеризует документ в текстуру (размер и видимая область в пикселях холста).
|
||||||
|
pub fn render(self: RenderEngine, document: *const Document, canvas_size: basic_models.ImageSize, visible_rect: basic_models.ImageRect) !?dvui.Texture {
|
||||||
|
return switch (self) {
|
||||||
|
.cpu => |cpu_r| cpu_r.renderDocument(document, canvas_size, visible_rect),
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
8
src/render/cpu/arc.zig
Normal file
8
src/render/cpu/arc.zig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
const Document = @import("../../models/Document.zig");
|
||||||
|
const pipeline = @import("pipeline.zig");
|
||||||
|
const DrawContext = pipeline.DrawContext;
|
||||||
|
|
||||||
|
const Object = Document.Object;
|
||||||
|
|
||||||
|
/// Рисует дугу (заглушка: пока не реализовано).
|
||||||
|
pub fn draw(_: *DrawContext, _: *const Object) void {}
|
||||||
22
src/render/cpu/broken.zig
Normal file
22
src/render/cpu/broken.zig
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Document = @import("../../models/Document.zig");
|
||||||
|
const pipeline = @import("pipeline.zig");
|
||||||
|
const line = @import("line.zig");
|
||||||
|
const DrawContext = pipeline.DrawContext;
|
||||||
|
const Color = @import("dvui").Color;
|
||||||
|
|
||||||
|
const Object = Document.Object;
|
||||||
|
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
|
||||||
|
|
||||||
|
/// Рисует ломаную по точкам в локальных координатах. Обводка по stroke_rgba.
|
||||||
|
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
|
||||||
|
const p_prop = obj.getProperty(.points) orelse return;
|
||||||
|
const pts = p_prop.points.items;
|
||||||
|
if (pts.len < 2) return;
|
||||||
|
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i + 1 < pts.len) : (i += 1) {
|
||||||
|
line.drawLine(ctx, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, stroke);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/render/cpu/draw.zig
Normal file
76
src/render/cpu/draw.zig
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Document = @import("../../models/Document.zig");
|
||||||
|
const pipeline = @import("pipeline.zig");
|
||||||
|
const line = @import("line.zig");
|
||||||
|
const ellipse = @import("ellipse.zig");
|
||||||
|
const broken = @import("broken.zig");
|
||||||
|
const arc = @import("arc.zig");
|
||||||
|
const basic_models = @import("../../models/basic_models.zig");
|
||||||
|
const ImageRect = basic_models.ImageRect;
|
||||||
|
const ImageSize = basic_models.ImageSize;
|
||||||
|
|
||||||
|
const Object = Document.Object;
|
||||||
|
const DrawContext = pipeline.DrawContext;
|
||||||
|
const Transform = pipeline.Transform;
|
||||||
|
|
||||||
|
fn getLocalTransform(obj: *const Object) Transform {
|
||||||
|
const pos = if (obj.getProperty(.position)) |p| p.position else basic_models.Point2{ .x = 0, .y = 0 };
|
||||||
|
const angle = if (obj.getProperty(.angle)) |p| p.angle else 0;
|
||||||
|
const scale = if (obj.getProperty(.scale)) |p| p.scale else basic_models.Scale2{ .scale_x = 1, .scale_y = 1 };
|
||||||
|
const opacity = if (obj.getProperty(.opacity)) |p| p.opacity else 1.0;
|
||||||
|
return .{
|
||||||
|
.position = pos,
|
||||||
|
.angle = angle,
|
||||||
|
.scale = scale,
|
||||||
|
.opacity = opacity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isVisible(obj: *const Object) bool {
|
||||||
|
return if (obj.getProperty(.visible)) |p| p.visible else true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drawObject(ctx: *DrawContext, obj: *const Object, parent_transform: Transform) void {
|
||||||
|
if (!isVisible(obj)) return;
|
||||||
|
const local = getLocalTransform(obj);
|
||||||
|
const world = Transform.compose(parent_transform, local);
|
||||||
|
ctx.setTransform(world);
|
||||||
|
|
||||||
|
switch (obj.shape) {
|
||||||
|
.line => line.draw(ctx, obj),
|
||||||
|
.ellipse => ellipse.draw(ctx, obj),
|
||||||
|
.broken => broken.draw(ctx, obj),
|
||||||
|
.arc => arc.draw(ctx, obj),
|
||||||
|
}
|
||||||
|
|
||||||
|
for (obj.children.items) |*child| {
|
||||||
|
drawObject(ctx, child, world);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Рекурсивно рисует документ в буфер: сначала корневые объекты по порядку, затем их потомков (каждый следующий поверх предыдущего).
|
||||||
|
pub fn drawDocument(
|
||||||
|
pixels: []@import("dvui").Color.PMA,
|
||||||
|
buf_width: u32,
|
||||||
|
buf_height: u32,
|
||||||
|
visible_rect: ImageRect,
|
||||||
|
document: *const Document,
|
||||||
|
canvas_size: ImageSize,
|
||||||
|
) void {
|
||||||
|
const scale_x: f32 = if (document.size.width > 0) @as(f32, @floatFromInt(canvas_size.w)) / document.size.width else 0;
|
||||||
|
const scale_y: f32 = if (document.size.height > 0) @as(f32, @floatFromInt(canvas_size.h)) / document.size.height else 0;
|
||||||
|
|
||||||
|
var ctx = DrawContext{
|
||||||
|
.pixels = pixels,
|
||||||
|
.buf_width = buf_width,
|
||||||
|
.buf_height = buf_height,
|
||||||
|
.visible_rect = visible_rect,
|
||||||
|
.scale_x = scale_x,
|
||||||
|
.scale_y = scale_y,
|
||||||
|
};
|
||||||
|
|
||||||
|
const identity = Transform{};
|
||||||
|
for (document.objects.items) |*obj| {
|
||||||
|
drawObject(&ctx, obj, identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/render/cpu/ellipse.zig
Normal file
59
src/render/cpu/ellipse.zig
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Document = @import("../../models/Document.zig");
|
||||||
|
const pipeline = @import("pipeline.zig");
|
||||||
|
const DrawContext = pipeline.DrawContext;
|
||||||
|
const Color = @import("dvui").Color;
|
||||||
|
|
||||||
|
const Object = Document.Object;
|
||||||
|
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
|
||||||
|
|
||||||
|
/// Рисует эллипс: центр (0,0), полуоси radii. Растеризация в координатах буфера (обводка кольцом).
|
||||||
|
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
|
||||||
|
const r_prop = obj.getProperty(.radii) orelse return;
|
||||||
|
const rx = r_prop.radii.x;
|
||||||
|
const ry = r_prop.radii.y;
|
||||||
|
if (rx <= 0 or ry <= 0) return;
|
||||||
|
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke;
|
||||||
|
|
||||||
|
const corners = [_]struct { x: f32, y: f32 }{
|
||||||
|
.{ .x = -rx, .y = -ry },
|
||||||
|
.{ .x = rx, .y = -ry },
|
||||||
|
.{ .x = rx, .y = ry },
|
||||||
|
.{ .x = -rx, .y = ry },
|
||||||
|
};
|
||||||
|
const w0 = ctx.localToWorld(corners[0].x, corners[0].y);
|
||||||
|
const b0 = ctx.worldToBufferF(w0.x, w0.y);
|
||||||
|
var min_bx: f32 = b0.x;
|
||||||
|
var min_by: f32 = b0.y;
|
||||||
|
var max_bx: f32 = b0.x;
|
||||||
|
var max_by: f32 = b0.y;
|
||||||
|
for (corners[1..]) |c| {
|
||||||
|
const w = ctx.localToWorld(c.x, c.y);
|
||||||
|
const b = ctx.worldToBufferF(w.x, w.y);
|
||||||
|
min_bx = @min(min_bx, b.x);
|
||||||
|
min_by = @min(min_by, b.y);
|
||||||
|
max_bx = @max(max_bx, b.x);
|
||||||
|
max_by = @max(max_by, b.y);
|
||||||
|
}
|
||||||
|
const buf_w: i32 = @intCast(ctx.buf_width);
|
||||||
|
const buf_h: i32 = @intCast(ctx.buf_height);
|
||||||
|
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 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);
|
||||||
|
|
||||||
|
var by: i32 = y0;
|
||||||
|
while (by < y1) : (by += 1) {
|
||||||
|
var bx: i32 = x0;
|
||||||
|
while (bx < x1) : (bx += 1) {
|
||||||
|
const w = ctx.bufferToWorld(@as(f32, @floatFromInt(bx)) + 0.5, @as(f32, @floatFromInt(by)) + 0.5);
|
||||||
|
const loc = ctx.worldToLocal(w.x, w.y);
|
||||||
|
const nx = loc.x / rx;
|
||||||
|
const ny = loc.y / ry;
|
||||||
|
const d = nx * nx + ny * ny;
|
||||||
|
if (d >= 0.9 and d <= 1.1) {
|
||||||
|
ctx.blendPixelAtBuffer(@intCast(bx), @intCast(by), stroke);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/render/cpu/line.zig
Normal file
60
src/render/cpu/line.zig
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Document = @import("../../models/Document.zig");
|
||||||
|
const pipeline = @import("pipeline.zig");
|
||||||
|
const DrawContext = pipeline.DrawContext;
|
||||||
|
const Color = @import("dvui").Color;
|
||||||
|
|
||||||
|
const Object = Document.Object;
|
||||||
|
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
|
||||||
|
|
||||||
|
/// Рисует линию в локальных координатах: от (0,0) до end_point. Растеризация в координатах буфера (без пробелов при зуме).
|
||||||
|
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
|
||||||
|
const ep_prop = obj.getProperty(.end_point) orelse return;
|
||||||
|
const end_x = ep_prop.end_point.x;
|
||||||
|
const end_y = ep_prop.end_point.y;
|
||||||
|
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke;
|
||||||
|
|
||||||
|
drawLine(ctx, 0, 0, end_x, end_y, stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Линия по локальным координатам фигуры: переводит концы в буфер и рисует в пикселях буфера.
|
||||||
|
pub fn drawLine(ctx: *DrawContext, x0: f32, y0: f32, x1: f32, y1: f32, color: Color.PMA) void {
|
||||||
|
const w0 = ctx.localToWorld(x0, y0);
|
||||||
|
const w1 = ctx.localToWorld(x1, y1);
|
||||||
|
const b0 = ctx.worldToBufferF(w0.x, w0.y);
|
||||||
|
const b1 = ctx.worldToBufferF(w1.x, w1.y);
|
||||||
|
const bx0: i32 = @intFromFloat(std.math.round(b0.x));
|
||||||
|
const by0: i32 = @intFromFloat(std.math.round(b0.y));
|
||||||
|
const bx1: i32 = @intFromFloat(std.math.round(b1.x));
|
||||||
|
const by1: i32 = @intFromFloat(std.math.round(b1.y));
|
||||||
|
drawLineInBuffer(ctx, bx0, by0, bx1, by1, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Брезенхем в координатах буфера; пиксели вне [0, buf_width) x [0, buf_height) пропускаются.
|
||||||
|
pub fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, color: Color.PMA) void {
|
||||||
|
const bw: i32 = @intCast(ctx.buf_width);
|
||||||
|
const bh: i32 = @intCast(ctx.buf_height);
|
||||||
|
const dx: i32 = @intCast(@abs(bx1 - bx0));
|
||||||
|
const dy: i32 = -@as(i32, @intCast(@abs(by1 - by0)));
|
||||||
|
const sx: i32 = if (bx0 < bx1) 1 else -1;
|
||||||
|
const sy: i32 = if (by0 < by1) 1 else -1;
|
||||||
|
var err = dx + dy;
|
||||||
|
var x = bx0;
|
||||||
|
var y = by0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (x >= 0 and x < bw and y >= 0 and y < bh) {
|
||||||
|
ctx.blendPixelAtBuffer(@intCast(x), @intCast(y), color);
|
||||||
|
}
|
||||||
|
if (x == bx1 and y == by1) break;
|
||||||
|
const e2 = 2 * err;
|
||||||
|
if (e2 >= dy) {
|
||||||
|
err += dy;
|
||||||
|
x += sx;
|
||||||
|
}
|
||||||
|
if (e2 <= dx) {
|
||||||
|
err += dx;
|
||||||
|
y += sy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
148
src/render/cpu/pipeline.zig
Normal file
148
src/render/cpu/pipeline.zig
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const dvui = @import("dvui");
|
||||||
|
const basic_models = @import("../../models/basic_models.zig");
|
||||||
|
const Point2 = basic_models.Point2;
|
||||||
|
const Scale2 = basic_models.Scale2;
|
||||||
|
const ImageRect = basic_models.ImageRect;
|
||||||
|
const Color = dvui.Color;
|
||||||
|
|
||||||
|
/// Трансформ объекта в мировых координатах документа (позиция, угол, масштаб, непрозрачность).
|
||||||
|
pub const Transform = struct {
|
||||||
|
position: Point2 = .{},
|
||||||
|
angle: f32 = 0,
|
||||||
|
scale: Scale2 = .{},
|
||||||
|
opacity: f32 = 1.0,
|
||||||
|
|
||||||
|
/// Композиция: мировой трансформ = parent * local (local в пространстве родителя).
|
||||||
|
pub fn compose(parent: Transform, local: Transform) Transform {
|
||||||
|
const cos_a = std.math.cos(parent.angle);
|
||||||
|
const sin_a = std.math.sin(parent.angle);
|
||||||
|
const sx = parent.scale.scale_x * local.scale.scale_x;
|
||||||
|
const sy = parent.scale.scale_y * local.scale.scale_y;
|
||||||
|
const local_px = local.position.x * parent.scale.scale_x;
|
||||||
|
const local_py = local.position.y * parent.scale.scale_y;
|
||||||
|
const rx = cos_a * local_px - sin_a * local_py;
|
||||||
|
const ry = sin_a * local_px + cos_a * local_py;
|
||||||
|
return .{
|
||||||
|
.position = .{
|
||||||
|
.x = parent.position.x + rx,
|
||||||
|
.y = parent.position.y + ry,
|
||||||
|
},
|
||||||
|
.angle = parent.angle + local.angle,
|
||||||
|
.scale = .{ .scale_x = sx, .scale_y = sy },
|
||||||
|
.opacity = parent.opacity * local.opacity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Единый конвейер: принимает позицию в локальных координатах фигуры и цвет пикселя,
|
||||||
|
/// применяет трансформ (вращение, масштаб, перенос) и непрозрачность, накладывает на буфер.
|
||||||
|
pub const DrawContext = struct {
|
||||||
|
pixels: []Color.PMA,
|
||||||
|
buf_width: u32,
|
||||||
|
buf_height: u32,
|
||||||
|
visible_rect: ImageRect,
|
||||||
|
scale_x: f32,
|
||||||
|
scale_y: f32,
|
||||||
|
transform: Transform = .{},
|
||||||
|
|
||||||
|
pub fn setTransform(self: *DrawContext, t: Transform) void {
|
||||||
|
self.transform = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Локальные координаты фигуры -> мировые (документ).
|
||||||
|
pub fn localToWorld(self: *const DrawContext, local_x: f32, local_y: f32) Point2 {
|
||||||
|
const t = &self.transform;
|
||||||
|
const cos_a = std.math.cos(t.angle);
|
||||||
|
const sin_a = std.math.sin(t.angle);
|
||||||
|
return .{
|
||||||
|
.x = t.position.x + (local_x * t.scale.scale_x) * cos_a - (local_y * t.scale.scale_y) * sin_a,
|
||||||
|
.y = t.position.y + (local_x * t.scale.scale_x) * sin_a + (local_y * t.scale.scale_y) * cos_a,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Мировые координаты документа -> координаты в буфере (могут быть вне [0, buf_w] x [0, buf_h]).
|
||||||
|
pub fn worldToBufferF(self: *const DrawContext, wx: f32, wy: f32) Point2 {
|
||||||
|
const canvas_x = wx * self.scale_x;
|
||||||
|
const canvas_y = wy * self.scale_y;
|
||||||
|
const vx = @as(f32, @floatFromInt(self.visible_rect.x));
|
||||||
|
const vy = @as(f32, @floatFromInt(self.visible_rect.y));
|
||||||
|
return .{
|
||||||
|
.x = canvas_x - vx,
|
||||||
|
.y = canvas_y - vy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Координаты буфера -> мировые (документ). scale_x/scale_y не должны быть 0.
|
||||||
|
pub fn bufferToWorld(self: *const DrawContext, buf_x: f32, buf_y: f32) Point2 {
|
||||||
|
const vx = @as(f32, @floatFromInt(self.visible_rect.x));
|
||||||
|
const vy = @as(f32, @floatFromInt(self.visible_rect.y));
|
||||||
|
const canvas_x = buf_x + vx;
|
||||||
|
const canvas_y = buf_y + vy;
|
||||||
|
const sx = if (self.scale_x != 0) self.scale_x else 1.0;
|
||||||
|
const sy = if (self.scale_y != 0) self.scale_y else 1.0;
|
||||||
|
return .{
|
||||||
|
.x = canvas_x / sx,
|
||||||
|
.y = canvas_y / sy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Мировые координаты -> локальные фигуры (обратное к localToWorld).
|
||||||
|
pub fn worldToLocal(self: *const DrawContext, wx: f32, wy: f32) Point2 {
|
||||||
|
const t = &self.transform;
|
||||||
|
const dx = wx - t.position.x;
|
||||||
|
const dy = wy - t.position.y;
|
||||||
|
const cos_a = std.math.cos(-t.angle);
|
||||||
|
const sin_a = std.math.sin(-t.angle);
|
||||||
|
const sx = if (t.scale.scale_x != 0) t.scale.scale_x else 1.0;
|
||||||
|
const sy = if (t.scale.scale_y != 0) t.scale.scale_y else 1.0;
|
||||||
|
return .{
|
||||||
|
.x = (dx * cos_a - dy * sin_a) / sx,
|
||||||
|
.y = (dx * sin_a + dy * cos_a) / sy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Смешивает цвет в пикселе буфера (bx, by) с учётом opacity текущего трансформа. Bounds не проверяются.
|
||||||
|
pub fn blendPixelAtBuffer(self: *DrawContext, bx: u32, by: u32, color: Color.PMA) void {
|
||||||
|
if (bx >= self.buf_width or by >= self.buf_height) return;
|
||||||
|
const t = &self.transform;
|
||||||
|
const idx = by * self.buf_width + bx;
|
||||||
|
const dst = &self.pixels[idx];
|
||||||
|
const a = @as(f32, @floatFromInt(color.a)) / 255.0 * t.opacity;
|
||||||
|
const src_r = @as(f32, @floatFromInt(color.r)) * a;
|
||||||
|
const src_g = @as(f32, @floatFromInt(color.g)) * a;
|
||||||
|
const src_b = @as(f32, @floatFromInt(color.b)) * a;
|
||||||
|
const inv_a = 1.0 - a;
|
||||||
|
dst.r = @intFromFloat(std.math.clamp(src_r + inv_a * @as(f32, @floatFromInt(dst.r)), 0, 255));
|
||||||
|
dst.g = @intFromFloat(std.math.clamp(src_g + inv_a * @as(f32, @floatFromInt(dst.g)), 0, 255));
|
||||||
|
dst.b = @intFromFloat(std.math.clamp(src_b + inv_a * @as(f32, @floatFromInt(dst.b)), 0, 255));
|
||||||
|
dst.a = @intFromFloat(std.math.clamp(a * 255 + inv_a * @as(f32, @floatFromInt(dst.a)), 0, 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Записывает пиксель в локальных координатах фигуры с учётом трансформа и прозрачности (PMA blend).
|
||||||
|
pub fn blendPixelLocal(self: *DrawContext, local_x: f32, local_y: f32, color: Color.PMA) void {
|
||||||
|
const w = self.localToWorld(local_x, local_y);
|
||||||
|
const b = self.worldToBufferF(w.x, w.y);
|
||||||
|
const bx: i32 = @intFromFloat(b.x);
|
||||||
|
const by: i32 = @intFromFloat(b.y);
|
||||||
|
const vw = @as(i32, @intCast(self.visible_rect.w));
|
||||||
|
const vh = @as(i32, @intCast(self.visible_rect.h));
|
||||||
|
if (bx < 0 or bx >= vw or by < 0 or by >= vh) return;
|
||||||
|
self.blendPixelAtBuffer(@intCast(bx), @intCast(by), color);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn rgbaToPma(rgba: u32) Color.PMA {
|
||||||
|
const r: u8 = @intCast((rgba >> 0) & 0xFF);
|
||||||
|
const g: u8 = @intCast((rgba >> 8) & 0xFF);
|
||||||
|
const b: u8 = @intCast((rgba >> 16) & 0xFF);
|
||||||
|
const a: u8 = @intCast((rgba >> 24) & 0xFF);
|
||||||
|
if (a == 0) return .{ .r = 0, .g = 0, .b = 0, .a = 0 };
|
||||||
|
const af: f32 = @as(f32, @floatFromInt(a)) / 255.0;
|
||||||
|
return .{
|
||||||
|
.r = @intFromFloat(@as(f32, @floatFromInt(r)) * af),
|
||||||
|
.g = @intFromFloat(@as(f32, @floatFromInt(g)) * af),
|
||||||
|
.b = @intFromFloat(@as(f32, @floatFromInt(b)) * af),
|
||||||
|
.a = a,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -112,13 +112,18 @@ pub fn leftPanel(ctx: *WindowContext) void {
|
|||||||
if (active_doc) |doc| {
|
if (active_doc) |doc| {
|
||||||
const canvas = &doc.canvas;
|
const canvas = &doc.canvas;
|
||||||
if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {}
|
if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {}
|
||||||
|
if (dvui.checkbox(@src(), &canvas.draw_document, "Draw document", .{})) {
|
||||||
|
canvas.requestRedraw();
|
||||||
|
}
|
||||||
|
if (!canvas.draw_document) {
|
||||||
if (dvui.button(@src(), if (doc.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) {
|
if (dvui.button(@src(), if (doc.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) {
|
||||||
if (doc.cpu_render.type == .Gradient) {
|
if (doc.cpu_render.type == .Gradient) {
|
||||||
doc.cpu_render.type = .Squares;
|
doc.cpu_render.type = .Squares;
|
||||||
} else {
|
} else {
|
||||||
doc.cpu_render.type = .Gradient;
|
doc.cpu_render.type = .Gradient;
|
||||||
}
|
}
|
||||||
canvas.redrawExample() catch {};
|
canvas.requestRedraw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dvui.label(@src(), "No document", .{}, .{});
|
dvui.label(@src(), "No document", .{}, .{});
|
||||||
|
|||||||
Reference in New Issue
Block a user