Compare commits
9 Commits
8532114673
...
6ae927c4b7
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ae927c4b7 | |||
| c73d710513 | |||
| 0b287e800d | |||
| b6012f1fc4 | |||
| 85a3bac095 | |||
| bee9513ba0 | |||
| 48824532f1 | |||
| 7dc7069186 | |||
| fd0ba8b583 |
@@ -3,31 +3,32 @@ const builtin = @import("builtin");
|
||||
const dvui = @import("dvui");
|
||||
const Document = @import("models/Document.zig");
|
||||
const RenderEngine = @import("render/RenderEngine.zig").RenderEngine;
|
||||
const ImageRect = @import("models/rasterization_models.zig").ImageRect;
|
||||
const Size = dvui.Size;
|
||||
const ImageRect = @import("models/basic_models.zig").ImageRect;
|
||||
const Color = dvui.Color;
|
||||
|
||||
const Canvas = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
document: *Document,
|
||||
render_engine: RenderEngine,
|
||||
texture: ?dvui.Texture = null,
|
||||
size: Size = .{ .w = 800, .h = 600 },
|
||||
pos: dvui.Point = dvui.Point{ .x = 0, .y = 0 },
|
||||
pos: dvui.Point = dvui.Point{ .x = 400, .y = 400 },
|
||||
scroll: dvui.ScrollInfo = .{
|
||||
.vertical = .auto,
|
||||
.horizontal = .auto,
|
||||
},
|
||||
native_scaling: bool = true,
|
||||
gradient_start: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 },
|
||||
gradient_end: Color.PMA = .{ .r = 255, .g = 255, .b = 255, .a = 255 },
|
||||
document: ?*Document = null,
|
||||
render_engine: RenderEngine,
|
||||
/// Максимальная частота перерисовки при зуме. 0 = без ограничения.
|
||||
redraw_throttle_ms: u32 = 50,
|
||||
_visible_rect: ?ImageRect = null,
|
||||
_zoom: f32 = 1,
|
||||
_redraw_pending: bool = false,
|
||||
_last_redraw_time_ms: i64 = 0,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, engine: RenderEngine) Canvas {
|
||||
pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.document = document,
|
||||
.render_engine = engine,
|
||||
};
|
||||
}
|
||||
@@ -63,6 +64,7 @@ pub fn redrawExample(self: *Canvas) !void {
|
||||
|
||||
self.texture = tex;
|
||||
}
|
||||
self._last_redraw_time_ms = std.time.milliTimestamp();
|
||||
}
|
||||
|
||||
// Ресетнуть example изображение в renderEngine
|
||||
@@ -80,12 +82,33 @@ pub fn addZoom(self: *Canvas, value: f32) void {
|
||||
self._zoom = @max(self._zoom, 0.01);
|
||||
}
|
||||
|
||||
/// Запросить перерисовку (выполнится с учётом redraw_throttle_ms при вызове processPendingRedraw).
|
||||
pub fn requestRedraw(self: *Canvas) void {
|
||||
self._redraw_pending = true;
|
||||
}
|
||||
|
||||
/// Выполнить отложенную перерисовку, если прошло не менее redraw_throttle_ms с прошлой отрисовки.
|
||||
pub fn processPendingRedraw(self: *Canvas) !void {
|
||||
if (!self._redraw_pending) return;
|
||||
if (self.redraw_throttle_ms == 0) {
|
||||
self._redraw_pending = false;
|
||||
try self.redrawExample();
|
||||
return;
|
||||
}
|
||||
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;
|
||||
if (elapsed < @as(i64, @intCast(self.redraw_throttle_ms))) return;
|
||||
self._redraw_pending = false;
|
||||
try self.redrawExample();
|
||||
}
|
||||
|
||||
pub fn getScaledImageSize(self: Canvas) ImageRect {
|
||||
const doc = self.document;
|
||||
return .{
|
||||
.x = @intFromFloat(self.pos.x),
|
||||
.y = @intFromFloat(self.pos.y),
|
||||
.w = @intFromFloat(self.size.w * self._zoom),
|
||||
.h = @intFromFloat(self.size.h * self._zoom),
|
||||
.w = @intFromFloat(doc.size.width * self._zoom),
|
||||
.h = @intFromFloat(doc.size.height * self._zoom),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,7 +128,7 @@ pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset:
|
||||
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) {
|
||||
try self.redrawExample();
|
||||
requestRedraw(self);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,99 @@
|
||||
const std = @import("std");
|
||||
const Canvas = @import("Canvas.zig");
|
||||
const CpuRenderEngine = @import("render/CpuRenderEngine.zig");
|
||||
const RenderEngine = @import("render/RenderEngine.zig").RenderEngine;
|
||||
const Document = @import("models/Document.zig");
|
||||
const basic_models = @import("models/basic_models.zig");
|
||||
|
||||
const WindowContext = @This();
|
||||
|
||||
/// Один открытый документ: документ + свой холст и движок рендера
|
||||
pub const OpenDocument = struct {
|
||||
document: Document,
|
||||
cpu_render: CpuRenderEngine,
|
||||
canvas: Canvas,
|
||||
|
||||
/// Инициализировать по месту (canvas хранит указатель на cpu_render этого же экземпляра).
|
||||
pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void {
|
||||
const default_size = basic_models.Size{ .width = 800, .height = 600 };
|
||||
self.document = Document.init(allocator, default_size);
|
||||
self.cpu_render = CpuRenderEngine.init(allocator, .Squares);
|
||||
self.canvas = Canvas.init(allocator, &self.document, (&self.cpu_render).renderEngine());
|
||||
}
|
||||
|
||||
pub fn deinit(self: *OpenDocument) void {
|
||||
self.document.deinit();
|
||||
self.canvas.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
canvas: Canvas,
|
||||
cpu_render: *CpuRenderEngine,
|
||||
frame_index: u64,
|
||||
/// Список открытых документов (вкладок): указатели на документ+холст
|
||||
documents: std.ArrayList(*OpenDocument),
|
||||
/// Индекс активной вкладки; null — ни один документ не выбран
|
||||
active_document_index: ?usize,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !WindowContext {
|
||||
var self: WindowContext = undefined;
|
||||
self.allocator = allocator;
|
||||
|
||||
self.cpu_render = try allocator.create(CpuRenderEngine);
|
||||
errdefer allocator.destroy(self.cpu_render);
|
||||
self.cpu_render.* = CpuRenderEngine.init(allocator, .Squares);
|
||||
|
||||
self.canvas = Canvas.init(allocator, self.cpu_render.renderEngine());
|
||||
|
||||
self.frame_index = 0;
|
||||
|
||||
return self;
|
||||
const frame_index: u64 = 0;
|
||||
const documents = std.ArrayList(*OpenDocument).empty;
|
||||
const active_document_index: ?usize = null;
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.frame_index = frame_index,
|
||||
.documents = documents,
|
||||
.active_document_index = active_document_index,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *WindowContext) void {
|
||||
self.canvas.deinit();
|
||||
self.allocator.destroy(self.cpu_render);
|
||||
for (self.documents.items) |ptr| {
|
||||
ptr.deinit();
|
||||
self.allocator.destroy(ptr);
|
||||
}
|
||||
self.documents.deinit(self.allocator);
|
||||
}
|
||||
|
||||
/// Вернуть указатель на активный открытый документ (null если нет выбранной вкладки).
|
||||
pub fn activeDocument(self: *WindowContext) ?*OpenDocument {
|
||||
const i = self.active_document_index orelse return null;
|
||||
if (i >= self.documents.items.len) return null;
|
||||
return self.documents.items[i];
|
||||
}
|
||||
|
||||
/// Добавить новый документ и сделать его активным.
|
||||
pub fn addNewDocument(self: *WindowContext) !void {
|
||||
const ptr = try self.allocator.create(OpenDocument);
|
||||
errdefer self.allocator.destroy(ptr);
|
||||
OpenDocument.init(self.allocator, ptr);
|
||||
try self.documents.append(self.allocator, ptr);
|
||||
self.active_document_index = self.documents.items.len - 1;
|
||||
}
|
||||
|
||||
/// Выбрать вкладку по индексу.
|
||||
pub fn setActiveDocument(self: *WindowContext, index: usize) void {
|
||||
if (index < self.documents.items.len) {
|
||||
self.active_document_index = index;
|
||||
}
|
||||
}
|
||||
|
||||
/// Закрыть вкладку по индексу; активная вкладка сдвигается при необходимости.
|
||||
pub fn closeDocument(self: *WindowContext, index: usize) void {
|
||||
if (index >= self.documents.items.len) return;
|
||||
const open_doc = self.documents.items[index];
|
||||
open_doc.deinit();
|
||||
self.allocator.destroy(open_doc);
|
||||
_ = self.documents.orderedRemove(index);
|
||||
|
||||
if (self.active_document_index) |*active| {
|
||||
if (index < active.*) {
|
||||
active.* -= 1;
|
||||
} else if (index == active.*) {
|
||||
if (self.documents.items.len > 0) {
|
||||
active.* = @min(index, self.documents.items.len - 1);
|
||||
} else {
|
||||
self.active_document_index = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
85
src/main.zig
85
src/main.zig
@@ -3,8 +3,7 @@ const builtin = @import("builtin");
|
||||
const dvui = @import("dvui");
|
||||
const dvui_ext = @import("ui/dvui_ext.zig");
|
||||
const SDLBackend = @import("sdl-backend");
|
||||
const Document = @import("models/Document.zig");
|
||||
const ImageRect = @import("models/rasterization_models.zig").ImageRect;
|
||||
const ImageRect = @import("models/basic_models.zig").ImageRect;
|
||||
const WindowContext = @import("WindowContext.zig");
|
||||
const sdl_c = SDLBackend.c;
|
||||
const Allocator = std.mem.Allocator;
|
||||
@@ -70,8 +69,8 @@ pub fn main() !void {
|
||||
}
|
||||
|
||||
fn gui_frame(ctx: *WindowContext) bool {
|
||||
const canvas = &ctx.canvas;
|
||||
const ctrl: bool = dvui.currentWindow().modifiers.control();
|
||||
const active_doc = ctx.activeDocument();
|
||||
|
||||
for (dvui.events()) |*e| {
|
||||
if (e.evt == .window and e.evt.window.action == .close) return false;
|
||||
@@ -80,34 +79,55 @@ fn gui_frame(ctx: *WindowContext) bool {
|
||||
|
||||
const root = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .horizontal },
|
||||
.{ .dir = .vertical },
|
||||
.{ .expand = .both, .background = true, .style = .window },
|
||||
);
|
||||
defer root.deinit();
|
||||
|
||||
// Левая панель с фиксированной шириной
|
||||
// Верхняя строка: таббар (вкладки документов + кнопка "Новый")
|
||||
var tab_bar = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal, .min_size_content = .{ .h = 32 }, .background = true, .padding = dvui.Rect.all(4) });
|
||||
{
|
||||
for (ctx.documents.items, 0..) |open_doc, i| {
|
||||
_ = open_doc;
|
||||
var buf: [32]u8 = undefined;
|
||||
const label = std.fmt.bufPrint(&buf, "Doc {d}", .{i + 1}) catch "Doc";
|
||||
if (dvui.button(@src(), label, .{}, .{ .id_extra = i })) {
|
||||
ctx.setActiveDocument(i);
|
||||
}
|
||||
}
|
||||
if (dvui.button(@src(), "+", .{}, .{})) {
|
||||
ctx.addNewDocument() catch |err| {
|
||||
std.debug.print("addNewDocument error: {}\n", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
tab_bar.deinit();
|
||||
|
||||
// Нижняя строка: левая панель + контент
|
||||
var content_row = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .both }); // no background to not override
|
||||
{
|
||||
// Левая панель с фиксированной шириной (инструменты для активного документа)
|
||||
var left_panel = dvui.box(@src(), .{ .dir = .vertical }, .{ .expand = .vertical, .min_size_content = .{ .w = 200 }, .background = true });
|
||||
{
|
||||
dvui.label(@src(), "Tools", .{}, .{});
|
||||
if (dvui.button(@src(), "Fill Random Color", .{}, .{}) or ctx.frame_index == 0) {
|
||||
canvas.exampleReset() catch |err| {
|
||||
std.debug.print("Error reset example: {}\n", .{err});
|
||||
};
|
||||
canvas.pos = .{ .x = 400, .y = 400 };
|
||||
}
|
||||
if (active_doc) |doc| {
|
||||
const canvas = &doc.canvas;
|
||||
if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {}
|
||||
if (dvui.button(@src(), if (ctx.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) {
|
||||
if (ctx.cpu_render.type == .Gradient) {
|
||||
ctx.cpu_render.type = .Squares;
|
||||
if (dvui.button(@src(), if (doc.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) {
|
||||
if (doc.cpu_render.type == .Gradient) {
|
||||
doc.cpu_render.type = .Squares;
|
||||
} else {
|
||||
ctx.cpu_render.type = .Gradient;
|
||||
doc.cpu_render.type = .Gradient;
|
||||
}
|
||||
canvas.redrawExample() catch {};
|
||||
}
|
||||
} else {
|
||||
dvui.label(@src(), "No document", .{}, .{});
|
||||
}
|
||||
}
|
||||
left_panel.deinit();
|
||||
|
||||
// Правая панель - занимает оставшееся пространство
|
||||
// Правая панель — контент выбранного документа или заглушка
|
||||
const back = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .horizontal },
|
||||
@@ -127,6 +147,8 @@ fn gui_frame(ctx: *WindowContext) bool {
|
||||
},
|
||||
);
|
||||
{
|
||||
if (active_doc) |doc| {
|
||||
const canvas = &doc.canvas;
|
||||
var textured = dvui_ext.texturedBox(right_panel.data().contentRectScale(), dvui.Rect.all(20));
|
||||
{
|
||||
var overlay = dvui.overlay(
|
||||
@@ -150,12 +172,9 @@ fn gui_frame(ctx: *WindowContext) bool {
|
||||
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
||||
const img_size = canvas.getScaledImageSize();
|
||||
|
||||
// Получить viewport и scroll offset
|
||||
const viewport_rect = scroll.data().contentRect();
|
||||
const scroll_current = dvui.Point{ .x = canvas.scroll.viewport.x, .y = canvas.scroll.viewport.y };
|
||||
|
||||
// viewport_rect/scroll_current — в natural единицах.
|
||||
// Для расчёта видимой области в пикселях изображения переводим в physical.
|
||||
const viewport_px = dvui.Rect{
|
||||
.x = viewport_rect.x * natural_scale,
|
||||
.y = viewport_rect.y * natural_scale,
|
||||
@@ -170,11 +189,10 @@ fn gui_frame(ctx: *WindowContext) bool {
|
||||
canvas.updateVisibleImageRect(viewport_px, scroll_px) catch |err| {
|
||||
std.debug.print("updateVisibleImageRect error: {}\n", .{err});
|
||||
};
|
||||
canvas.processPendingRedraw() catch |err| {
|
||||
std.debug.print("processPendingRedraw error: {}\n", .{err});
|
||||
};
|
||||
|
||||
// `canvas.texture` contains ONLY the visible part.
|
||||
// If we render it inside a widget sized as the full image, dvui will stretch it.
|
||||
// Instead: create a scroll content surface sized like the full image, then place
|
||||
// the visible texture at the correct offset at 1:1.
|
||||
const content_w_px: u32 = img_size.x + img_size.w;
|
||||
const content_h_px: u32 = img_size.y + img_size.h;
|
||||
const content_w = @as(f32, @floatFromInt(content_w_px)) / natural_scale;
|
||||
@@ -213,7 +231,6 @@ fn gui_frame(ctx: *WindowContext) bool {
|
||||
}
|
||||
canvas_layer.deinit();
|
||||
|
||||
// Заблокировать события скролла, если нажат ctrl
|
||||
if (ctrl) {
|
||||
for (dvui.events()) |*e| {
|
||||
switch (e.evt) {
|
||||
@@ -223,7 +240,7 @@ fn gui_frame(ctx: *WindowContext) bool {
|
||||
switch (action) {
|
||||
.wheel_y => |y| {
|
||||
canvas.addZoom(y / 1000);
|
||||
canvas.redrawExample() catch {};
|
||||
canvas.requestRedraw();
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
@@ -242,10 +259,28 @@ fn gui_frame(ctx: *WindowContext) bool {
|
||||
overlay.deinit();
|
||||
}
|
||||
textured.deinit();
|
||||
} else {
|
||||
var no_doc_center = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
.{ .expand = .both, .padding = dvui.Rect.all(20) },
|
||||
);
|
||||
{
|
||||
dvui.label(@src(), "No document open", .{}, .{});
|
||||
if (dvui.button(@src(), "New document", .{}, .{})) {
|
||||
ctx.addNewDocument() catch |err| {
|
||||
std.debug.print("addNewDocument error: {}\n", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
no_doc_center.deinit();
|
||||
}
|
||||
}
|
||||
right_panel.deinit();
|
||||
}
|
||||
back.deinit();
|
||||
}
|
||||
content_row.deinit();
|
||||
|
||||
ctx.frame_index += 1;
|
||||
|
||||
|
||||
@@ -1 +1,134 @@
|
||||
// Файл векторного документа
|
||||
const std = @import("std");
|
||||
const basic_models = @import("basic_models.zig");
|
||||
const properties = @import("Property.zig");
|
||||
const Property = properties.Property;
|
||||
const Size = basic_models.Size;
|
||||
const Document = @This();
|
||||
|
||||
size: Size,
|
||||
allocator: std.mem.Allocator,
|
||||
/// Корневые объекты документа (вложенность через Object.children).
|
||||
objects: std.ArrayList(Object),
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, size: Size) Document {
|
||||
return .{
|
||||
.size = size,
|
||||
.allocator = allocator,
|
||||
.objects = std.ArrayList(Object).empty,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Document) void {
|
||||
for (self.objects.items) |*obj| obj.deinit(self.allocator);
|
||||
self.objects.deinit(self.allocator);
|
||||
}
|
||||
|
||||
/// Добавить корневой объект в документ (клонирует в allocator документа).
|
||||
pub fn addObject(self: *Document, template: Object) !void {
|
||||
const obj = try template.clone(self.allocator);
|
||||
try self.objects.append(self.allocator, obj);
|
||||
}
|
||||
|
||||
/// Тип фигуры: определяет, как RenderEngine интерпретирует свойства и рисует объект.
|
||||
pub const ShapeKind = enum {
|
||||
rect,
|
||||
ellipse,
|
||||
line,
|
||||
path,
|
||||
};
|
||||
|
||||
/// Объект документа: тип фигуры, свойства и вложенные дочерние объекты.
|
||||
/// Типы объектов задаются конструкторами; UI и RenderEngine работают с union Property.
|
||||
pub const Object = struct {
|
||||
shape: ShapeKind,
|
||||
properties: std.ArrayList(Property),
|
||||
/// Вложенные объекты (дерево).
|
||||
children: std.ArrayList(Object),
|
||||
|
||||
/// Найти свойство по тегу и вернуть вариант union (caller делает switch).
|
||||
pub fn getProperty(self: Object, tag: std.meta.Tag(Property)) ?Property {
|
||||
for (self.properties.items) |prop| {
|
||||
if (std.meta.activeTag(prop) == tag) return prop;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Установить свойство: если уже есть с таким тегом — заменить, иначе добавить.
|
||||
pub fn setProperty(self: *Object, allocator: std.mem.Allocator, prop: Property) !void {
|
||||
for (self.properties.items, 0..) |*p, i| {
|
||||
if (std.meta.activeTag(p.*) == std.meta.activeTag(prop)) {
|
||||
self.properties.items[i] = prop;
|
||||
return;
|
||||
}
|
||||
}
|
||||
try self.properties.append(allocator, prop);
|
||||
}
|
||||
|
||||
/// Добавить дочерний объект (клонирует в переданный allocator).
|
||||
pub fn addChild(self: *Object, allocator: std.mem.Allocator, template: Object) !void {
|
||||
const obj = try template.clone(allocator);
|
||||
try self.children.append(allocator, obj);
|
||||
}
|
||||
|
||||
/// Клонировать объект рекурсивно (свойства и дети).
|
||||
pub fn clone(self: Object, allocator: std.mem.Allocator) !Object {
|
||||
var properties_list = std.ArrayList(Property).empty;
|
||||
errdefer properties_list.deinit(allocator);
|
||||
try properties_list.appendSlice(allocator, self.properties.items);
|
||||
|
||||
var children_list = std.ArrayList(Object).empty;
|
||||
errdefer children_list.deinit(allocator);
|
||||
for (self.children.items) |child| {
|
||||
try children_list.append(allocator, try child.clone(allocator));
|
||||
}
|
||||
|
||||
return .{
|
||||
.shape = self.shape,
|
||||
.properties = properties_list,
|
||||
.children = children_list,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Object, allocator: std.mem.Allocator) void {
|
||||
for (self.children.items) |*child| child.deinit(allocator);
|
||||
self.children.deinit(allocator);
|
||||
self.properties.deinit(allocator);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Базовый объект с общим набором свойств (для внутреннего использования конструкторами).
|
||||
fn createWithCommon(allocator: std.mem.Allocator, shape: ShapeKind) !Object {
|
||||
const common = properties.defaultCommonProperties();
|
||||
var properties_list = std.ArrayList(Property).empty;
|
||||
errdefer properties_list.deinit(allocator);
|
||||
try properties_list.appendSlice(allocator, &common);
|
||||
return .{
|
||||
.shape = shape,
|
||||
.properties = properties_list,
|
||||
.children = std.ArrayList(Object).empty,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Публичные конструкторы: базовый объект + одно свойство фигуры ---
|
||||
|
||||
pub fn createRect(allocator: std.mem.Allocator) !Object {
|
||||
var obj = try createWithCommon(allocator, .rect);
|
||||
errdefer obj.deinit(allocator);
|
||||
try obj.properties.append(allocator, .{ .size = .{ .width = 100, .height = 100 } });
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn createEllipse(allocator: std.mem.Allocator) !Object {
|
||||
var obj = try createWithCommon(allocator, .ellipse);
|
||||
errdefer obj.deinit(allocator);
|
||||
try obj.properties.append(allocator, .{ .radii = .{ .x = 50, .y = 50 } });
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn createLine(allocator: std.mem.Allocator) !Object {
|
||||
var obj = try createWithCommon(allocator, .line);
|
||||
errdefer obj.deinit(allocator);
|
||||
try obj.properties.append(allocator, .{ .end_point = .{ .x = 100, .y = 0 } });
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
|
||||
62
src/models/Property.zig
Normal file
62
src/models/Property.zig
Normal file
@@ -0,0 +1,62 @@
|
||||
// Модель свойств объекта документа.
|
||||
// Каждое свойство — отдельный тип в union; UI и RenderEngine работают с полиморфным Property.
|
||||
// Комплексные значения (размер, радиусы) — один вариант свойства, а не несколько полей.
|
||||
|
||||
const basic_models = @import("basic_models.zig");
|
||||
const Point2 = basic_models.Point2;
|
||||
const Scale2 = basic_models.Scale2;
|
||||
const Size = basic_models.Size;
|
||||
const Radii = basic_models.Radii;
|
||||
|
||||
/// Одно свойство объекта: полиморфный union.
|
||||
/// Варианты — целостные значения (size, radii), не разбитые на отдельные поля.
|
||||
/// UI перебирает свойства и по тегу показывает нужный редактор;
|
||||
/// RenderEngine по тегу фигуры читает нужные свойства для отрисовки.
|
||||
pub const Property = union(enum) {
|
||||
// --- Общие для всех фигур (см. defaultCommonProperties) ---
|
||||
/// Левый верхний угол bbox для rect/path; центр для ellipse; начало для line.
|
||||
position: Point2,
|
||||
scale: Scale2,
|
||||
visible: bool,
|
||||
opacity: f32,
|
||||
locked: bool,
|
||||
|
||||
// --- Прямоугольник: один вариант ---
|
||||
size: Size,
|
||||
|
||||
// --- Эллипс: один вариант ---
|
||||
radii: Radii,
|
||||
|
||||
// --- Линия: конечная точка (относительно position) ---
|
||||
end_point: Point2,
|
||||
|
||||
// --- Визуал (опционально для будущего) ---
|
||||
fill_rgba: u32,
|
||||
stroke_rgba: u32,
|
||||
};
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// Общий набор свойств по умолчанию для любого объекта (одно место определения).
|
||||
/// Конструкторы фигур добавляют сначала его, затем специфичные свойства.
|
||||
pub fn defaultCommonProperties() []Property {
|
||||
return .{
|
||||
.{ .position = .{ .x = 0, .y = 0 } },
|
||||
.{ .scale = .{ .scale_x = 1, .scale_y = 1 } },
|
||||
.{ .visible = true },
|
||||
.{ .opacity = 1.0 },
|
||||
.{ .locked = false },
|
||||
};
|
||||
}
|
||||
|
||||
test "Property is union" {
|
||||
const p: Property = .{ .opacity = 0.5 };
|
||||
try std.testing.expect(p == .opacity);
|
||||
try std.testing.expect(p.opacity == 0.5);
|
||||
}
|
||||
|
||||
test "common properties" {
|
||||
const common = defaultCommonProperties();
|
||||
try std.testing.expect(common[0].position.x == 0);
|
||||
try std.testing.expect(common[2].visible == true);
|
||||
}
|
||||
34
src/models/basic_models.zig
Normal file
34
src/models/basic_models.zig
Normal file
@@ -0,0 +1,34 @@
|
||||
pub const ImageRect = struct {
|
||||
x: u32,
|
||||
y: u32,
|
||||
w: u32,
|
||||
h: u32,
|
||||
};
|
||||
|
||||
pub const ImageSize = struct {
|
||||
w: u32,
|
||||
h: u32,
|
||||
};
|
||||
|
||||
pub const Size = struct {
|
||||
width: f32,
|
||||
height: f32,
|
||||
};
|
||||
|
||||
/// Точка в 2D (документные единицы)
|
||||
pub const Point2 = struct {
|
||||
x: f32 = 0,
|
||||
y: f32 = 0,
|
||||
};
|
||||
|
||||
/// Радиусы эллипса по осям (одно свойство).
|
||||
pub const Radii = struct {
|
||||
x: f32,
|
||||
y: f32,
|
||||
};
|
||||
|
||||
/// Масштаб объекта
|
||||
pub const Scale2 = struct {
|
||||
scale_x: f32 = 1,
|
||||
scale_y: f32 = 1,
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
pub const ImageRect = struct {
|
||||
x: u32,
|
||||
y: u32,
|
||||
w: u32,
|
||||
h: u32,
|
||||
};
|
||||
|
||||
pub const ImageSize = struct {
|
||||
w: u32,
|
||||
h: u32,
|
||||
};
|
||||
@@ -2,9 +2,9 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const dvui = @import("dvui");
|
||||
const RenderEngine = @import("RenderEngine.zig").RenderEngine;
|
||||
const rast_models = @import("../models/rasterization_models.zig");
|
||||
const ImageSize = rast_models.ImageSize;
|
||||
const ImageRect = rast_models.ImageRect;
|
||||
const basic_models = @import("../models/basic_models.zig");
|
||||
const ImageSize = basic_models.ImageSize;
|
||||
const ImageRect = basic_models.ImageRect;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Color = dvui.Color;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Интерфейс для рендеринга документа
|
||||
const dvui = @import("dvui");
|
||||
const CpuRenderEngine = @import("CpuRenderEngine.zig");
|
||||
const rast_models = @import("../models/rasterization_models.zig");
|
||||
const basic_models = @import("../models/basic_models.zig");
|
||||
|
||||
pub const RenderEngine = union(enum) {
|
||||
cpu: *CpuRenderEngine,
|
||||
@@ -12,7 +12,7 @@ pub const RenderEngine = union(enum) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn example(self: RenderEngine, canvas_size: rast_models.ImageSize, visible_rect: rast_models.ImageRect) !?dvui.Texture {
|
||||
pub fn example(self: RenderEngine, canvas_size: basic_models.ImageSize, visible_rect: basic_models.ImageRect) !?dvui.Texture {
|
||||
return switch (self) {
|
||||
.cpu => |cpu_r| cpu_r.example(canvas_size, visible_rect),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user