Compare commits
5 Commits
1dda9c9d15
...
1a94cc8bfd
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a94cc8bfd | |||
| aeda3ee0d0 | |||
| dd9d5deb92 | |||
| bd58286c98 | |||
| b896a67fd4 |
@@ -4,6 +4,7 @@ const dvui = @import("dvui");
|
||||
const Document = @import("models/Document.zig");
|
||||
const RenderEngine = @import("render/RenderEngine.zig").RenderEngine;
|
||||
const ImageRect = @import("models/basic_models.zig").ImageRect;
|
||||
const Point2 = @import("models/basic_models.zig").Point2;
|
||||
const Color = dvui.Color;
|
||||
|
||||
const Canvas = @This();
|
||||
@@ -18,12 +19,12 @@ scroll: dvui.ScrollInfo = .{
|
||||
.horizontal = .auto,
|
||||
},
|
||||
native_scaling: bool = true,
|
||||
/// Максимальная частота перерисовки при зуме. 0 = без ограничения.
|
||||
redraw_throttle_ms: u32 = 50,
|
||||
_visible_rect: ?ImageRect = null,
|
||||
_zoom: f32 = 1,
|
||||
_redraw_pending: bool = false,
|
||||
_last_redraw_time_ms: i64 = 0,
|
||||
cursor_document_point: ?Point2 = null,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas {
|
||||
return .{
|
||||
@@ -40,9 +41,8 @@ pub fn deinit(self: *Canvas) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Заполнить canvas градиентом
|
||||
pub fn redrawExample(self: *Canvas) !void {
|
||||
const full = self.getScaledImageSize();
|
||||
const full = self.getZoomedImageSize();
|
||||
|
||||
const vis: ImageRect = self._visible_rect orelse ImageRect{ .x = 0, .y = 0, .w = 0, .h = 0 };
|
||||
|
||||
@@ -57,7 +57,6 @@ pub fn redrawExample(self: *Canvas) !void {
|
||||
const new_texture = self.render_engine.example(.{ .w = full.w, .h = full.h }, vis) catch null;
|
||||
|
||||
if (new_texture) |tex| {
|
||||
// Удалить старую текстуру
|
||||
if (self.texture) |old_tex| {
|
||||
dvui.Texture.destroyLater(old_tex);
|
||||
}
|
||||
@@ -67,7 +66,6 @@ pub fn redrawExample(self: *Canvas) !void {
|
||||
self._last_redraw_time_ms = std.time.milliTimestamp();
|
||||
}
|
||||
|
||||
// Ресетнуть example изображение в renderEngine
|
||||
pub fn exampleReset(self: *Canvas) !void {
|
||||
self.render_engine.exampleReset();
|
||||
try self.redrawExample();
|
||||
@@ -82,12 +80,10 @@ 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) {
|
||||
@@ -102,7 +98,7 @@ pub fn processPendingRedraw(self: *Canvas) !void {
|
||||
try self.redrawExample();
|
||||
}
|
||||
|
||||
pub fn getScaledImageSize(self: Canvas) ImageRect {
|
||||
pub fn getZoomedImageSize(self: Canvas) ImageRect {
|
||||
const doc = self.document;
|
||||
return .{
|
||||
.x = @intFromFloat(self.pos.x),
|
||||
@@ -112,11 +108,25 @@ pub fn getScaledImageSize(self: Canvas) ImageRect {
|
||||
};
|
||||
}
|
||||
|
||||
/// Обновить видимую часть изображения (в пикселях холста) и сохранить в `visible_rect`.
|
||||
///
|
||||
/// `viewport` и `scroll_offset` ожидаются в *physical* пикселях (т.е. уже умноженные на windowNaturalScale).
|
||||
///
|
||||
/// После обновления (или если текстуры ещё нет) перерисовывает текстуру, чтобы она содержала только видимую часть.
|
||||
pub fn contentPointToDocument(self: Canvas, content_point: dvui.Point, natural_scale: f32) ?Point2 {
|
||||
const img = self.getZoomedImageSize();
|
||||
const left_n = @as(f32, @floatFromInt(img.x)) / natural_scale;
|
||||
const top_n = @as(f32, @floatFromInt(img.y)) / natural_scale;
|
||||
const right_n = @as(f32, @floatFromInt(img.x + img.w)) / natural_scale;
|
||||
const bottom_n = @as(f32, @floatFromInt(img.y + img.h)) / natural_scale;
|
||||
|
||||
if (content_point.x < left_n or content_point.x >= right_n or
|
||||
content_point.y < top_n or content_point.y >= bottom_n)
|
||||
return null;
|
||||
|
||||
const px_x = content_point.x * natural_scale - @as(f32, @floatFromInt(img.x));
|
||||
const px_y = content_point.y * natural_scale - @as(f32, @floatFromInt(img.y));
|
||||
return Point2{
|
||||
.x = px_x / self._zoom,
|
||||
.y = px_y / self._zoom,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) bool {
|
||||
const next = computeVisibleImageRect(self.*, viewport, scroll_offset);
|
||||
var changed = false;
|
||||
@@ -134,16 +144,14 @@ pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset:
|
||||
}
|
||||
|
||||
fn computeVisibleImageRect(self: Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) ImageRect {
|
||||
const image_rect = self.getScaledImageSize();
|
||||
const image_rect = self.getZoomedImageSize();
|
||||
|
||||
const img_w: u32 = image_rect.w;
|
||||
const img_h: u32 = image_rect.h;
|
||||
|
||||
// Видимый размер всегда равен размеру viewport, но не больше холста
|
||||
const vis_w: u32 = @min(@as(u32, @intFromFloat(viewport.w)), img_w);
|
||||
const vis_h: u32 = @min(@as(u32, @intFromFloat(viewport.h)), img_h);
|
||||
|
||||
// Вычисляем x и y на основе scroll_offset, clamped чтобы не выходить за границы
|
||||
const raw_x: i64 = @intFromFloat(scroll_offset.x - @as(f32, @floatFromInt(image_rect.x)));
|
||||
const raw_y: i64 = @intFromFloat(scroll_offset.y - @as(f32, @floatFromInt(image_rect.y)));
|
||||
|
||||
|
||||
@@ -7,13 +7,11 @@ 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);
|
||||
@@ -29,9 +27,7 @@ pub const OpenDocument = struct {
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
frame_index: u64,
|
||||
/// Список открытых документов (вкладок): указатели на документ+холст
|
||||
documents: std.ArrayList(*OpenDocument),
|
||||
/// Индекс активной вкладки; null — ни один документ не выбран
|
||||
active_document_index: ?usize,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !WindowContext {
|
||||
@@ -54,30 +50,27 @@ pub fn deinit(self: *WindowContext) void {
|
||||
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 ptr.document.addRandomShapes(std.crypto.random);
|
||||
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];
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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();
|
||||
|
||||
pub const Object = @import("Object.zig");
|
||||
|
||||
size: Size,
|
||||
allocator: std.mem.Allocator,
|
||||
/// Корневые объекты документа (вложенность через Object.children).
|
||||
objects: std.ArrayList(Object),
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, size: Size) Document {
|
||||
@@ -23,112 +22,61 @@ pub fn deinit(self: *Document) void {
|
||||
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 addShape(self: *Document, parent: ?*Object, shape: Object.ShapeKind) !void {
|
||||
const obj = switch (shape) {
|
||||
.line => try Object.createLine(self.allocator),
|
||||
.ellipse => try Object.createEllipse(self.allocator),
|
||||
.broken => try Object.createBrokenLine(self.allocator),
|
||||
.arc => return error.ArcNotImplemented,
|
||||
};
|
||||
if (parent) |p| {
|
||||
try p.addChild(self.allocator, obj);
|
||||
} else {
|
||||
try self.addObject(obj);
|
||||
}
|
||||
}
|
||||
|
||||
fn randomShapeKind(rng: std.Random) Object.ShapeKind {
|
||||
const shapes_implemented = [_]Object.ShapeKind{ .line, .ellipse, .broken };
|
||||
return shapes_implemented[rng.intRangeLessThan(usize, 0, shapes_implemented.len)];
|
||||
}
|
||||
|
||||
/// Создаёт случайное количество фигур в документе (в т.ч. вложенных).
|
||||
/// Используются только реализованные типы: line, ellipse, broken.
|
||||
/// Ограничение max_total предотвращает экспоненциальный рост и переполнение.
|
||||
pub fn addRandomShapes(self: *Document, rng: std.Random) !void {
|
||||
const max_total: usize = 80;
|
||||
var total_count: usize = 0;
|
||||
|
||||
const n_root = rng.intRangeLessThan(usize, 1, 5);
|
||||
for (0..n_root) |_| {
|
||||
if (total_count >= max_total) break;
|
||||
try self.addShape(null, randomShapeKind(rng));
|
||||
total_count += 1;
|
||||
}
|
||||
|
||||
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;
|
||||
var stack = std.ArrayList(*Object).empty;
|
||||
defer stack.deinit(self.allocator);
|
||||
for (self.objects.items) |*obj| {
|
||||
try stack.append(self.allocator, obj);
|
||||
}
|
||||
|
||||
/// Базовый объект с общим набором свойств (для внутреннего использования конструкторами).
|
||||
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,
|
||||
};
|
||||
while (stack.pop()) |obj| {
|
||||
if (total_count >= max_total) continue;
|
||||
const n_children = rng.intRangeLessThan(usize, 0, 2);
|
||||
const base_len = obj.children.items.len;
|
||||
for (0..n_children) |_| {
|
||||
if (total_count >= max_total) break;
|
||||
try self.addShape(obj, randomShapeKind(rng));
|
||||
total_count += 1;
|
||||
}
|
||||
|
||||
// --- Публичные конструкторы: базовый объект + одно свойство фигуры ---
|
||||
|
||||
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;
|
||||
// Пушим в стек только после всех append, чтобы не держать указатели при реаллокации obj.children
|
||||
for (obj.children.items[base_len..]) |*child| {
|
||||
try stack.append(self.allocator, child);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
108
src/models/Object.zig
Normal file
108
src/models/Object.zig
Normal file
@@ -0,0 +1,108 @@
|
||||
const std = @import("std");
|
||||
const basic_models = @import("basic_models.zig");
|
||||
const Size = basic_models.Size;
|
||||
const Point2 = basic_models.Point2;
|
||||
const Property = @import("Property.zig").Property;
|
||||
const PropertyData = @import("Property.zig").Data;
|
||||
const defaultCommonProperties = @import("Property.zig").defaultCommonProperties;
|
||||
const Object = @This();
|
||||
|
||||
pub const ShapeKind = enum {
|
||||
line,
|
||||
ellipse,
|
||||
arc,
|
||||
broken,
|
||||
};
|
||||
|
||||
shape: ShapeKind,
|
||||
properties: std.ArrayList(Property),
|
||||
children: std.ArrayList(Object),
|
||||
|
||||
pub fn getProperty(self: Object, tag: std.meta.Tag(PropertyData)) ?*const PropertyData {
|
||||
for (self.properties.items) |*prop| {
|
||||
if (std.meta.activeTag(prop.data) == tag) return &prop.data;
|
||||
}
|
||||
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.data) == std.meta.activeTag(prop.data)) {
|
||||
if (p.data == .points) p.data.points.deinit(allocator);
|
||||
self.properties.items[i] = prop;
|
||||
return;
|
||||
}
|
||||
}
|
||||
try self.properties.append(allocator, prop);
|
||||
}
|
||||
|
||||
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);
|
||||
for (self.properties.items) |prop| {
|
||||
try properties_list.append(allocator, try prop.clone(allocator));
|
||||
}
|
||||
|
||||
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);
|
||||
for (self.properties.items) |*prop| prop.deinit(allocator);
|
||||
self.properties.deinit(allocator);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
fn createWithCommonProperties(allocator: std.mem.Allocator, shape: ShapeKind) !Object {
|
||||
var properties_list = std.ArrayList(Property).empty;
|
||||
errdefer properties_list.deinit(allocator);
|
||||
for (defaultCommonProperties) |prop| try properties_list.append(allocator, prop);
|
||||
return .{
|
||||
.shape = shape,
|
||||
.properties = properties_list,
|
||||
.children = std.ArrayList(Object).empty,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn createEllipse(allocator: std.mem.Allocator) !Object {
|
||||
var obj = try createWithCommonProperties(allocator, .ellipse);
|
||||
errdefer obj.deinit(allocator);
|
||||
try obj.properties.append(allocator, .{ .data = .{ .radii = .{ .x = 50, .y = 50 } } });
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn createLine(allocator: std.mem.Allocator) !Object {
|
||||
var obj = try createWithCommonProperties(allocator, .line);
|
||||
errdefer obj.deinit(allocator);
|
||||
try obj.properties.append(allocator, .{ .data = .{ .end_point = .{ .x = 100, .y = 0 } } });
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn createBrokenLine(allocator: std.mem.Allocator) !Object {
|
||||
var obj = try createWithCommonProperties(allocator, .broken);
|
||||
errdefer obj.deinit(allocator);
|
||||
var points = std.ArrayList(Point2).empty;
|
||||
try points.appendSlice(allocator, &.{
|
||||
.{ .x = 0, .y = 0 },
|
||||
.{ .x = 80, .y = 0 },
|
||||
.{ .x = 80, .y = 60 },
|
||||
});
|
||||
try obj.properties.append(allocator, .{ .data = .{ .points = points } });
|
||||
return obj;
|
||||
}
|
||||
@@ -1,62 +1,77 @@
|
||||
// Модель свойств объекта документа.
|
||||
// Каждое свойство — отдельный тип в union; UI и RenderEngine работают с полиморфным Property.
|
||||
// Комплексные значения (размер, радиусы) — один вариант свойства, а не несколько полей.
|
||||
|
||||
const std = @import("std");
|
||||
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.
|
||||
pub const Data = union(enum) {
|
||||
position: Point2,
|
||||
angle: f32,
|
||||
scale: Scale2,
|
||||
visible: bool,
|
||||
opacity: f32,
|
||||
locked: bool,
|
||||
|
||||
// --- Прямоугольник: один вариант ---
|
||||
size: Size,
|
||||
|
||||
// --- Эллипс: один вариант ---
|
||||
radii: Radii,
|
||||
|
||||
// --- Линия: конечная точка (относительно position) ---
|
||||
end_point: Point2,
|
||||
|
||||
// --- Визуал (опционально для будущего) ---
|
||||
points: std.ArrayList(Point2),
|
||||
|
||||
fill_rgba: u32,
|
||||
stroke_rgba: u32,
|
||||
};
|
||||
|
||||
const std = @import("std");
|
||||
pub const Property = struct {
|
||||
data: Data,
|
||||
|
||||
/// Общий набор свойств по умолчанию для любого объекта (одно место определения).
|
||||
/// Конструкторы фигур добавляют сначала его, затем специфичные свойства.
|
||||
pub fn defaultCommonProperties() []Property {
|
||||
return .{
|
||||
pub fn deinit(self: *Property, allocator: std.mem.Allocator) void {
|
||||
switch (self.data) {
|
||||
.points => |*list| list.deinit(allocator),
|
||||
else => {},
|
||||
}
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
pub fn clone(self: Property, allocator: std.mem.Allocator) !Property {
|
||||
return switch (self.data) {
|
||||
.points => |list| .{
|
||||
.data = .{
|
||||
.points = try list.clone(allocator),
|
||||
},
|
||||
},
|
||||
else => .{ .data = self.data },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const default_common_data = [_]Data{
|
||||
.{ .position = .{ .x = 0, .y = 0 } },
|
||||
.{ .angle = 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);
|
||||
pub const defaultCommonProperties: [default_common_data.len]Property = blk: {
|
||||
var result: [default_common_data.len]Property = undefined;
|
||||
for (default_common_data, &result) |d, *p| {
|
||||
p.* = .{ .data = d };
|
||||
}
|
||||
break :blk result;
|
||||
};
|
||||
|
||||
test "Property wrapper and Data" {
|
||||
const p = Property{ .data = .{ .opacity = 0.5 } };
|
||||
try std.testing.expect(p.data == .opacity);
|
||||
try std.testing.expect(p.data.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);
|
||||
try std.testing.expect(defaultCommonProperties[0].data == .position);
|
||||
try std.testing.expect(defaultCommonProperties[0].data.position.x == 0);
|
||||
try std.testing.expect(defaultCommonProperties[3].data == .visible);
|
||||
try std.testing.expect(defaultCommonProperties[3].data.visible == true);
|
||||
}
|
||||
|
||||
@@ -15,19 +15,16 @@ pub const Size = struct {
|
||||
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,
|
||||
|
||||
@@ -27,7 +27,6 @@ pub fn init(allocator: Allocator, render_type: Type) CpuRenderEngine {
|
||||
}
|
||||
|
||||
pub fn exampleReset(self: *CpuRenderEngine) void {
|
||||
// Сгенерировать случайные цвета градиента
|
||||
var prng = std.Random.DefaultPrng.init(@intCast(std.time.microTimestamp()));
|
||||
const random = prng.random();
|
||||
self.gradient_start = Color.PMA{ .r = random.int(u8), .g = random.int(u8), .b = random.int(u8), .a = 255 };
|
||||
@@ -64,12 +63,12 @@ fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: ImageS
|
||||
_ = self;
|
||||
|
||||
const colors = [_]Color.PMA{
|
||||
.{ .r = 255, .g = 0, .b = 0, .a = 255 }, // red
|
||||
.{ .r = 255, .g = 165, .b = 0, .a = 255 }, // orange
|
||||
.{ .r = 255, .g = 255, .b = 0, .a = 255 }, // yellow
|
||||
.{ .r = 0, .g = 255, .b = 0, .a = 255 }, // green
|
||||
.{ .r = 0, .g = 255, .b = 255, .a = 255 }, // cyan
|
||||
.{ .r = 0, .g = 0, .b = 255, .a = 255 }, // blue
|
||||
.{ .r = 255, .g = 0, .b = 0, .a = 255 },
|
||||
.{ .r = 255, .g = 165, .b = 0, .a = 255 },
|
||||
.{ .r = 255, .g = 255, .b = 0, .a = 255 },
|
||||
.{ .r = 0, .g = 255, .b = 0, .a = 255 },
|
||||
.{ .r = 0, .g = 255, .b = 255, .a = 255 },
|
||||
.{ .r = 0, .g = 0, .b = 255, .a = 255 },
|
||||
};
|
||||
|
||||
const squares_num = 5;
|
||||
@@ -106,7 +105,6 @@ fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: ImageS
|
||||
const canvas_x = x + visible_rect.x;
|
||||
if (canvas_x >= canvas_size.w) continue;
|
||||
|
||||
// Check vertical line index
|
||||
var vertical_index: ?u32 = null;
|
||||
for (0..x_pos.len) |i| {
|
||||
if (canvas_x >= x_pos[i] and canvas_x < x_pos[i] + thikness) {
|
||||
@@ -115,7 +113,6 @@ fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: ImageS
|
||||
}
|
||||
}
|
||||
|
||||
// Check horizontal line index
|
||||
var horizontal_index: ?u32 = null;
|
||||
for (0..y_pos.len) |i| {
|
||||
if (canvas_y >= y_pos[i] and canvas_y < y_pos[i] + thikness) {
|
||||
@@ -129,7 +126,6 @@ fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: ImageS
|
||||
} else if (horizontal_index) |idx| {
|
||||
pixels[y * visible_rect.w + x] = colors[idx];
|
||||
} else {
|
||||
// Find square
|
||||
var square_x: u32 = 0;
|
||||
for (0..squares_num) |i| {
|
||||
if (canvas_x >= x_pos[i] + thikness and canvas_x < x_pos[i + 1]) {
|
||||
@@ -161,12 +157,9 @@ pub fn example(self: CpuRenderEngine, canvas_size: ImageSize, visible_rect: Imag
|
||||
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);
|
||||
|
||||
// std.debug.print("w={any}, fw={any};\th={any}, fh={any}\n", .{ width, full_w, height, full_h });
|
||||
|
||||
switch (self.type) {
|
||||
.Gradient => self.renderGradient(pixels, width, height, full_w, full_h, visible_rect),
|
||||
.Squares => self.renderSquares(pixels, canvas_size, visible_rect),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Интерфейс для рендеринга документа
|
||||
const dvui = @import("dvui");
|
||||
const CpuRenderEngine = @import("CpuRenderEngine.zig");
|
||||
const basic_models = @import("../models/basic_models.zig");
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
// Test root for `zig build test`.
|
||||
// Import modules here to ensure their `test` blocks are discovered.
|
||||
|
||||
test "module test discovery" {
|
||||
_ = @import("render/CpuRenderEngine.zig");
|
||||
// Корень для `zig build test`. Тесты из импортированных здесь модулей выполняются (в Zig не подтягиваются из транзитивных импортов).
|
||||
// Добавляй сюда _ = @import("path/to/module.zig"); для каждого модуля с test-блоками.
|
||||
// Чтобы увидеть список всех тестов: после `zig build test` выполни `./zig-out/bin/test`.
|
||||
test "discover tests" {
|
||||
_ = @import("main.zig");
|
||||
_ = @import("models/Property.zig");
|
||||
}
|
||||
|
||||
// Убедиться, что выполнились все ожидаемые тесты: этот тест пройдёт только если до него дошли (т.е. все предыдущие прошли).
|
||||
test "all module tests completed" {
|
||||
const std = @import("std");
|
||||
std.debug.print("\n (все тесты модулей выполнены)\n", .{});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Виджет холста: скролл, текстура, зум по Ctrl+колёсико.
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
const dvui_ext = @import("dvui_ext.zig");
|
||||
@@ -22,6 +21,7 @@ pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void {
|
||||
{
|
||||
drawCanvasContent(canvas, scroll);
|
||||
handleCanvasZoom(canvas, scroll);
|
||||
handleCanvasMouse(canvas, scroll);
|
||||
}
|
||||
scroll.deinit();
|
||||
|
||||
@@ -34,7 +34,7 @@ pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void {
|
||||
|
||||
fn drawCanvasContent(canvas: *Canvas, scroll: anytype) void {
|
||||
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
||||
const img_size = canvas.getScaledImageSize();
|
||||
const img_size = canvas.getZoomedImageSize();
|
||||
const viewport_rect = scroll.data().contentRect();
|
||||
const scroll_current = dvui.Point{ .x = canvas.scroll.viewport.x, .y = canvas.scroll.viewport.y };
|
||||
|
||||
@@ -118,3 +118,26 @@ fn handleCanvasZoom(canvas: *Canvas, scroll: anytype) void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handleCanvasMouse(canvas: *Canvas, scroll: anytype) void {
|
||||
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
||||
|
||||
for (dvui.events()) |*e| {
|
||||
switch (e.evt) {
|
||||
.mouse => |*mouse| {
|
||||
if (mouse.action != .press or mouse.button != .left) continue;
|
||||
if (!dvui.eventMatchSimple(e, scroll.data())) continue;
|
||||
|
||||
const viewport_pt = scroll.data().contentRectScale().pointFromPhysical(mouse.p);
|
||||
const content_pt = dvui.Point{
|
||||
.x = viewport_pt.x + canvas.scroll.viewport.x,
|
||||
.y = viewport_pt.y + canvas.scroll.viewport.y,
|
||||
};
|
||||
canvas.cursor_document_point = canvas.contentPointToDocument(content_pt, natural_scale);
|
||||
if (canvas.cursor_document_point) |point|
|
||||
std.debug.print("cursor_document_point: {}\n", .{point});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Расширения для dvui
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
const TexturedBox = @import("./types/TexturedBox.zig");
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// Корневой кадр UI: разметка и сборка панелей.
|
||||
const dvui = @import("dvui");
|
||||
const WindowContext = @import("../WindowContext.zig");
|
||||
const tab_bar = @import("tab_bar.zig");
|
||||
const left_panel = @import("left_panel.zig");
|
||||
const right_panel = @import("right_panel.zig");
|
||||
|
||||
/// Отрисовать один кадр GUI. Возвращает false при закрытии окна/выходе.
|
||||
pub fn guiFrame(ctx: *WindowContext) bool {
|
||||
for (dvui.events()) |*e| {
|
||||
if (e.evt == .window and e.evt.window.action == .close) return false;
|
||||
@@ -24,16 +22,8 @@ pub fn guiFrame(ctx: *WindowContext) bool {
|
||||
{
|
||||
left_panel.leftPanel(ctx);
|
||||
|
||||
var back = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .horizontal },
|
||||
.{ .expand = .both, .padding = dvui.Rect.all(12), .background = true },
|
||||
);
|
||||
{
|
||||
right_panel.rightPanel(ctx);
|
||||
}
|
||||
back.deinit();
|
||||
}
|
||||
content_row.deinit();
|
||||
}
|
||||
root.deinit();
|
||||
|
||||
@@ -1,15 +1,112 @@
|
||||
// Левая панель: инструменты для активного документа (scaling, тип рендера).
|
||||
const dvui = @import("dvui");
|
||||
const WindowContext = @import("../WindowContext.zig");
|
||||
const Document = @import("../models/Document.zig");
|
||||
const Object = Document.Object;
|
||||
|
||||
const panel_gap: f32 = 12;
|
||||
const panel_padding: f32 = 5;
|
||||
const panel_radius: f32 = 24;
|
||||
const fill_color = dvui.Color.black.opacity(0.2);
|
||||
|
||||
fn shapeLabel(shape: Object.ShapeKind) []const u8 {
|
||||
return switch (shape) {
|
||||
.line => "Line",
|
||||
.ellipse => "Ellipse",
|
||||
.arc => "Arc",
|
||||
.broken => "Broken line",
|
||||
};
|
||||
}
|
||||
|
||||
fn objectTreeRow(obj: *const Object, depth: u32, row_id: *usize) void {
|
||||
const id = row_id.*;
|
||||
row_id.* += 1;
|
||||
const indent_px = depth * 18;
|
||||
var row = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .horizontal },
|
||||
.{ .padding = dvui.Rect{ .x = @floatFromInt(indent_px) }, .id_extra = id },
|
||||
);
|
||||
{
|
||||
dvui.labelNoFmt(@src(), shapeLabel(obj.shape), .{}, .{ .id_extra = id });
|
||||
}
|
||||
row.deinit();
|
||||
for (obj.children.items) |*child| {
|
||||
objectTreeRow(child, depth + 1, row_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn objectTree(ctx: *WindowContext) void {
|
||||
const active_doc = ctx.activeDocument();
|
||||
if (active_doc) |open_doc| {
|
||||
const doc = &open_doc.document;
|
||||
if (doc.objects.items.len == 0) {
|
||||
dvui.label(@src(), "No objects", .{}, .{});
|
||||
} else {
|
||||
var row_id: usize = 0;
|
||||
for (doc.objects.items) |*obj| {
|
||||
objectTreeRow(obj, 0, &row_id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dvui.label(@src(), "No document", .{}, .{});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn leftPanel(ctx: *WindowContext) void {
|
||||
var padding = dvui.Rect.all(panel_gap);
|
||||
padding.w = 0;
|
||||
var panel = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
.{ .expand = .vertical, .min_size_content = .{ .w = 200 }, .background = true },
|
||||
.{
|
||||
.expand = .vertical,
|
||||
.min_size_content = .{ .w = 220 },
|
||||
.background = true,
|
||||
.padding = padding,
|
||||
},
|
||||
);
|
||||
{
|
||||
dvui.label(@src(), "Tools", .{}, .{});
|
||||
// Верхняя часть: дерево объектов
|
||||
var tree_section = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
.{
|
||||
.expand = .both,
|
||||
.padding = dvui.Rect.all(panel_padding),
|
||||
.corner_radius = dvui.Rect.all(panel_radius),
|
||||
.color_fill = fill_color,
|
||||
.background = true,
|
||||
},
|
||||
);
|
||||
{
|
||||
dvui.label(@src(), "Objects", .{}, .{});
|
||||
var scroll = dvui.scrollArea(
|
||||
@src(),
|
||||
.{ .vertical = .auto },
|
||||
.{ .expand = .vertical, .background = false },
|
||||
);
|
||||
{
|
||||
objectTree(ctx);
|
||||
}
|
||||
scroll.deinit();
|
||||
}
|
||||
tree_section.deinit();
|
||||
|
||||
// Нижняя часть: настройки
|
||||
var settings_section = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
.{
|
||||
.expand = .horizontal,
|
||||
.margin = .{ .y = 5 },
|
||||
.padding = dvui.Rect.all(panel_padding),
|
||||
.corner_radius = dvui.Rect.all(panel_radius),
|
||||
.color_fill = fill_color,
|
||||
.background = true,
|
||||
},
|
||||
);
|
||||
{
|
||||
dvui.label(@src(), "Settings", .{}, .{});
|
||||
|
||||
const active_doc = ctx.activeDocument();
|
||||
if (active_doc) |doc| {
|
||||
@@ -27,5 +124,7 @@ pub fn leftPanel(ctx: *WindowContext) void {
|
||||
dvui.label(@src(), "No document", .{}, .{});
|
||||
}
|
||||
}
|
||||
settings_section.deinit();
|
||||
}
|
||||
panel.deinit();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Правая панель: контент документа (холст) или заглушка «Нет документа».
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
const WindowContext = @import("../WindowContext.zig");
|
||||
@@ -6,6 +5,12 @@ const canvas_view = @import("canvas_view.zig");
|
||||
|
||||
pub fn rightPanel(ctx: *WindowContext) void {
|
||||
const fill_color = dvui.Color.black.opacity(0.25);
|
||||
var back = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .horizontal },
|
||||
.{ .expand = .both, .padding = dvui.Rect.all(12), .background = true },
|
||||
);
|
||||
{
|
||||
var panel = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
@@ -27,13 +32,27 @@ pub fn rightPanel(ctx: *WindowContext) void {
|
||||
}
|
||||
}
|
||||
panel.deinit();
|
||||
}
|
||||
back.deinit();
|
||||
}
|
||||
|
||||
fn noDocView(ctx: *WindowContext) void {
|
||||
var center = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
.{ .expand = .both, .padding = dvui.Rect.all(20) },
|
||||
.{
|
||||
.expand = .both,
|
||||
.padding = dvui.Rect.all(20),
|
||||
},
|
||||
);
|
||||
{
|
||||
var box = dvui.box(
|
||||
@src(),
|
||||
.{ .dir = .vertical },
|
||||
.{
|
||||
.gravity_x = 0.5,
|
||||
.gravity_y = 0.5,
|
||||
},
|
||||
);
|
||||
{
|
||||
dvui.label(@src(), "No document open", .{}, .{});
|
||||
@@ -43,5 +62,7 @@ fn noDocView(ctx: *WindowContext) void {
|
||||
};
|
||||
}
|
||||
}
|
||||
box.deinit();
|
||||
}
|
||||
center.deinit();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Верхняя строка: вкладки документов + кнопка «Новый».
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
const WindowContext = @import("../WindowContext.zig");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Отрисовка дочернего контента как текстуры с параметрами скругления
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
const TexturedBox = @This();
|
||||
@@ -26,10 +25,6 @@ pub fn deinit(self: *TexturedBox) void {
|
||||
const tex = dvui.textureFromTarget(picture.texture) catch null;
|
||||
if (tex) |t| {
|
||||
dvui.Texture.destroyLater(t);
|
||||
// self.rs.r.y -= 2;
|
||||
// self.rs.r.x -= 2;
|
||||
// self.rs.r.h += 2;
|
||||
// self.rs.r.w += 2;
|
||||
dvui.renderTexture(t, self.rs, .{
|
||||
.corner_radius = self.corner_radius,
|
||||
}) catch {};
|
||||
|
||||
Reference in New Issue
Block a user