Compare commits
12 Commits
9202b527e4
...
05f5481a42
| Author | SHA1 | Date | |
|---|---|---|---|
| 05f5481a42 | |||
| 0eee436150 | |||
| 01b76ede64 | |||
| faf79367f6 | |||
| f8731bde87 | |||
| 563c2a7535 | |||
| b6e14cd74d | |||
| 790200be2a | |||
| 65ca468bfb | |||
| 317ebb958b | |||
| 31ba2c57cb | |||
| 5c9ec3167a |
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@@ -12,9 +12,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Zig: Debug (gdb)",
|
"name": "Zig: Debug (gdb)",
|
||||||
"type": "gdb",
|
"type": "cppdbg",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${workspaceFolder}/zig-out/bin/Zivro",
|
"program": "${workspaceFolder}/zig-out/bin/Zivro",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"MIMode": "gdb",
|
||||||
"preLaunchTask": "zig: build"
|
"preLaunchTask": "zig: build"
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
1
.vscode/tasks.json
vendored
1
.vscode/tasks.json
vendored
@@ -9,7 +9,6 @@
|
|||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"problemMatcher": ["$gcc"],
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ pub fn build(b: *std.Build) void {
|
|||||||
|
|
||||||
const exe = b.addExecutable(.{
|
const exe = b.addExecutable(.{
|
||||||
.name = "Zivro",
|
.name = "Zivro",
|
||||||
|
.use_llvm = true,
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
.root_source_file = b.path("src/main.zig"),
|
.root_source_file = b.path("src/main.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
|
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "dvui", .module = dvui_dep.module("dvui_sdl3") },
|
.{ .name = "dvui", .module = dvui_dep.module("dvui_sdl3") },
|
||||||
.{ .name = "sdl-backend", .module = dvui_dep.module("sdl3") },
|
.{ .name = "sdl-backend", .module = dvui_dep.module("sdl3") },
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ const Rect_i = basic_models.Rect_i;
|
|||||||
const Size_i = basic_models.Size_i;
|
const Size_i = basic_models.Size_i;
|
||||||
const Point2_f = @import("models/basic_models.zig").Point2_f;
|
const Point2_f = @import("models/basic_models.zig").Point2_f;
|
||||||
const Color = dvui.Color;
|
const Color = dvui.Color;
|
||||||
|
const tools = @import("tools.zig");
|
||||||
const Toolbar = @import("Toolbar.zig");
|
const Toolbar = @import("Toolbar.zig");
|
||||||
|
const random_document = @import("random_document.zig");
|
||||||
const Canvas = @This();
|
const Canvas = @This();
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
@@ -29,15 +30,16 @@ _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_f = null,
|
cursor_document_point: ?Point2_f = null,
|
||||||
/// true — рисовать документ (render), false — пример (gradient/squares).
|
|
||||||
draw_document: bool = true,
|
draw_document: bool = true,
|
||||||
|
/// Rect тулбара (из предыдущего кадра) для исключения кликов по нему из handleCanvasMouse.
|
||||||
|
toolbar_rect_scale: ?dvui.RectScale = null,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine, toolbar: Toolbar) Canvas {
|
pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas {
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.document = document,
|
.document = document,
|
||||||
.render_engine = engine,
|
.render_engine = engine,
|
||||||
.toolbar = toolbar,
|
.toolbar = Toolbar.init(&tools.default_tools),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +85,11 @@ pub fn exampleReset(self: *Canvas) !void {
|
|||||||
try self.redraw();
|
try self.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn addRandomShapes(self: *Canvas) !void {
|
||||||
|
try random_document.addRandomShapes(self.document, std.crypto.random);
|
||||||
|
self.requestRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn setZoom(self: *Canvas, value: f32) void {
|
pub fn setZoom(self: *Canvas, value: f32) void {
|
||||||
self._zoom = @max(value, 0.01);
|
self._zoom = @max(value, 0.01);
|
||||||
}
|
}
|
||||||
@@ -124,8 +131,7 @@ pub fn getZoomedImageSize(self: Canvas) Rect_i {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Возвращает координаты точки контента в координатах документа. Всегда возвращает точку,
|
/// Точка контента -> координаты документа.
|
||||||
/// даже если она за пределами документа
|
|
||||||
pub fn contentPointToDocument(self: Canvas, content_point: dvui.Point, natural_scale: f32) Point2_f {
|
pub fn contentPointToDocument(self: Canvas, content_point: dvui.Point, natural_scale: f32) Point2_f {
|
||||||
const img = self.getZoomedImageSize();
|
const img = self.getZoomedImageSize();
|
||||||
const px_x = content_point.x * natural_scale - @as(f32, @floatFromInt(img.x));
|
const px_x = content_point.x * natural_scale - @as(f32, @floatFromInt(img.x));
|
||||||
@@ -136,7 +142,7 @@ pub fn contentPointToDocument(self: Canvas, content_point: dvui.Point, natural_s
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Возвращает true, если точка контента лежит внутри холста (документа).
|
/// Точка контента внутри холста.
|
||||||
pub fn isContentPointOnDocument(self: Canvas, content_point: dvui.Point, natural_scale: f32) bool {
|
pub fn isContentPointOnDocument(self: Canvas, content_point: dvui.Point, natural_scale: f32) bool {
|
||||||
const img = self.getZoomedImageSize();
|
const img = self.getZoomedImageSize();
|
||||||
const left_n = @as(f32, @floatFromInt(img.x)) / natural_scale;
|
const left_n = @as(f32, @floatFromInt(img.x)) / natural_scale;
|
||||||
|
|||||||
11
src/Tool.zig
Normal file
11
src/Tool.zig
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const Point2_f = @import("models/basic_models.zig").Point2_f;
|
||||||
|
const Canvas = @import("Canvas.zig");
|
||||||
|
|
||||||
|
pub const ToolContext = struct {
|
||||||
|
canvas: *Canvas,
|
||||||
|
document_point: Point2_f,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Tool = struct {
|
||||||
|
onCanvasClick: *const fn (*const ToolContext) anyerror!void,
|
||||||
|
};
|
||||||
@@ -1,26 +1,20 @@
|
|||||||
//! Структура тулбара инструментов. Жизненный цикл совпадает с Canvas.
|
const Tool = @import("Tool.zig");
|
||||||
|
|
||||||
const tool_interface = @import("tool_interface.zig");
|
|
||||||
|
|
||||||
const Toolbar = @This();
|
const Toolbar = @This();
|
||||||
|
|
||||||
/// Описание одного инструмента для тулбара.
|
|
||||||
pub const ToolDescriptor = struct {
|
pub const ToolDescriptor = struct {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
/// Иконка в формате TVG (байты).
|
|
||||||
icon_tvg: []const u8,
|
icon_tvg: []const u8,
|
||||||
/// Реализация интерфейса инструмента (своя для каждого инструмента в tools/).
|
implementation: *const Tool.Tool,
|
||||||
implementation: *const tool_interface.Tool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Вертикальный тулбар инструментов.
|
|
||||||
tools: []const ToolDescriptor,
|
tools: []const ToolDescriptor,
|
||||||
selected_index: usize,
|
selected_index: ?usize,
|
||||||
|
|
||||||
pub fn init(tools_list: []const ToolDescriptor) Toolbar {
|
pub fn init(tools_list: []const ToolDescriptor) Toolbar {
|
||||||
return .{
|
return .{
|
||||||
.tools = tools_list,
|
.tools = tools_list,
|
||||||
.selected_index = 0,
|
.selected_index = null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,9 +22,22 @@ pub fn deinit(_: *Toolbar) void {}
|
|||||||
|
|
||||||
pub fn currentDescriptor(self: *const Toolbar) ?*const ToolDescriptor {
|
pub fn currentDescriptor(self: *const Toolbar) ?*const ToolDescriptor {
|
||||||
if (self.tools.len == 0) return null;
|
if (self.tools.len == 0) return null;
|
||||||
return &self.tools[self.selected_index];
|
if (self.selected_index) |index| {
|
||||||
|
return &self.tools[index];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select(self: *Toolbar, index: usize) void {
|
pub fn select(self: *Toolbar, index: ?usize) void {
|
||||||
if (index < self.tools.len) self.selected_index = index;
|
if (index == self.selected_index) {
|
||||||
|
self.selected_index = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index) |i| {
|
||||||
|
if (i < self.tools.len) {
|
||||||
|
self.selected_index = i;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.selected_index = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ const Canvas = @import("Canvas.zig");
|
|||||||
const CpuRenderEngine = @import("render/CpuRenderEngine.zig");
|
const CpuRenderEngine = @import("render/CpuRenderEngine.zig");
|
||||||
const RenderEngine = @import("render/RenderEngine.zig").RenderEngine;
|
const RenderEngine = @import("render/RenderEngine.zig").RenderEngine;
|
||||||
const Document = @import("models/Document.zig");
|
const Document = @import("models/Document.zig");
|
||||||
const random_document = @import("models/random_document.zig");
|
const random_document = @import("random_document.zig");
|
||||||
const basic_models = @import("models/basic_models.zig");
|
const basic_models = @import("models/basic_models.zig");
|
||||||
const tools = @import("tools.zig");
|
|
||||||
const Toolbar = @import("Toolbar.zig");
|
|
||||||
|
|
||||||
const WindowContext = @This();
|
const WindowContext = @This();
|
||||||
|
|
||||||
@@ -23,7 +21,6 @@ pub const OpenDocument = struct {
|
|||||||
allocator,
|
allocator,
|
||||||
&self.document,
|
&self.document,
|
||||||
(&self.cpu_render).renderEngine(),
|
(&self.cpu_render).renderEngine(),
|
||||||
Toolbar.init(&tools.default_tools),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +65,7 @@ pub fn addNewDocument(self: *WindowContext) !void {
|
|||||||
const ptr = try self.allocator.create(OpenDocument);
|
const ptr = try self.allocator.create(OpenDocument);
|
||||||
errdefer self.allocator.destroy(ptr);
|
errdefer self.allocator.destroy(ptr);
|
||||||
OpenDocument.init(self.allocator, ptr);
|
OpenDocument.init(self.allocator, ptr);
|
||||||
try random_document.addRandomShapes(&ptr.document, std.crypto.random);
|
//try random_document.addRandomShapes(&ptr.document, std.crypto.random);
|
||||||
try self.documents.append(self.allocator, ptr);
|
try self.documents.append(self.allocator, ptr);
|
||||||
self.active_document_index = self.documents.items.len - 1;
|
self.active_document_index = self.documents.items.len - 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
|
|
||||||
pub const line = dvui.entypo.line_graph;
|
pub const line = dvui.entypo.line_graph;
|
||||||
|
pub const ellipse = dvui.entypo.circle;
|
||||||
|
pub const arc = dvui.entypo.loop;
|
||||||
|
pub const broken = dvui.entypo.flow_line;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const default_common_data = [_]PropertyData{
|
|||||||
.{ .visible = true },
|
.{ .visible = true },
|
||||||
.{ .opacity = 1.0 },
|
.{ .opacity = 1.0 },
|
||||||
.{ .locked = false },
|
.{ .locked = false },
|
||||||
.{ .stroke_rgba = 0x000000FF },
|
.{ .stroke_rgba = 0x000000FF }, // чёрный, полная непрозрачность
|
||||||
.{ .thickness = 2.0 },
|
.{ .thickness = 2.0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ pub const Data = union(enum) {
|
|||||||
|
|
||||||
points: std.ArrayList(Point2_f),
|
points: std.ArrayList(Point2_f),
|
||||||
|
|
||||||
|
/// Цвет заливки, 0xRRGGBBAA.
|
||||||
fill_rgba: u32,
|
fill_rgba: u32,
|
||||||
|
/// Цвет обводки, 0xRRGGBBAA.
|
||||||
stroke_rgba: u32,
|
stroke_rgba: u32,
|
||||||
|
|
||||||
thickness: f32,
|
thickness: f32,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ pub const Point2_f = struct {
|
|||||||
y: f32 = 0,
|
y: f32 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Целочисленная точка (например, координаты в буфере пикселей).
|
|
||||||
pub const Point2_i = struct {
|
pub const Point2_i = struct {
|
||||||
x: i32 = 0,
|
x: i32 = 0,
|
||||||
y: i32 = 0,
|
y: i32 = 0,
|
||||||
@@ -36,7 +35,6 @@ pub const Scale2_f = struct {
|
|||||||
scale_y: f32 = 1,
|
scale_y: f32 = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Прямоугольник в координатах документа (f32), например локальные границы объекта.
|
|
||||||
pub const Rect_f = struct {
|
pub const Rect_f = struct {
|
||||||
x: f32 = 0,
|
x: f32 = 0,
|
||||||
y: f32 = 0,
|
y: f32 = 0,
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ const PropertyData = @import("../Property.zig").Data;
|
|||||||
const Rect_f = @import("../basic_models.zig").Rect_f;
|
const Rect_f = @import("../basic_models.zig").Rect_f;
|
||||||
const shape_mod = @import("shape.zig");
|
const shape_mod = @import("shape.zig");
|
||||||
|
|
||||||
/// Теги обязательных свойств (заглушка: arc пока не реализован).
|
/// Теги обязательных свойств (arc не реализован).
|
||||||
pub fn getRequiredTags() []const std.meta.Tag(PropertyData) {
|
pub fn getRequiredTags() []const std.meta.Tag(PropertyData) {
|
||||||
return &[_]std.meta.Tag(PropertyData){};
|
return &[_]std.meta.Tag(PropertyData){};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Добавляет свойства по умолчанию для дуги (заглушка).
|
/// Добавляет свойства по умолчанию для дуги.
|
||||||
pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) !void {
|
pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) !void {
|
||||||
_ = allocator;
|
_ = allocator;
|
||||||
_ = obj;
|
_ = obj;
|
||||||
return error.ArcNotImplemented;
|
return error.ArcNotImplemented;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Локальные границы дуги (заглушка: пока не реализовано).
|
/// Локальные границы дуги (не реализовано).
|
||||||
pub fn getLocalBounds(obj: *const Object) !Rect_f {
|
pub fn getLocalBounds(obj: *const Object) !Rect_f {
|
||||||
try shape_mod.ensure(obj, .arc);
|
try shape_mod.ensure(obj, .arc);
|
||||||
return error.ArcNotImplemented;
|
return error.ArcNotImplemented;
|
||||||
|
|||||||
@@ -6,28 +6,28 @@ const Point2_f = @import("../basic_models.zig").Point2_f;
|
|||||||
const Rect_f = @import("../basic_models.zig").Rect_f;
|
const Rect_f = @import("../basic_models.zig").Rect_f;
|
||||||
const shape_mod = @import("shape.zig");
|
const shape_mod = @import("shape.zig");
|
||||||
|
|
||||||
/// Точки ломаной по умолчанию (для создания).
|
/// Точки ломаной по умолчанию.
|
||||||
pub const default_points = [_]Point2_f{
|
pub const default_points = [_]Point2_f{
|
||||||
.{ .x = 0, .y = 0 },
|
.{ .x = 0, .y = 0 },
|
||||||
.{ .x = 80, .y = 0 },
|
.{ .x = 80, .y = 0 },
|
||||||
.{ .x = 80, .y = 60 },
|
.{ .x = 80, .y = 60 },
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Теги обязательных свойств (у ломаной нет const default_shape_properties, только default_points).
|
/// Теги обязательных свойств.
|
||||||
pub fn getRequiredTags() []const std.meta.Tag(PropertyData) {
|
pub fn getRequiredTags() []const std.meta.Tag(PropertyData) {
|
||||||
return &[_]std.meta.Tag(PropertyData){
|
return &[_]std.meta.Tag(PropertyData){
|
||||||
.points,
|
.points,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Добавляет к объекту свойства по умолчанию для ломаной (points из default_points).
|
/// Добавляет к объекту свойства по умолчанию для ломаной.
|
||||||
pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) !void {
|
pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) !void {
|
||||||
var points = std.ArrayList(Point2_f).empty;
|
var points = std.ArrayList(Point2_f).empty;
|
||||||
try points.appendSlice(allocator, &default_points);
|
try points.appendSlice(allocator, &default_points);
|
||||||
try obj.properties.append(allocator, .{ .data = .{ .points = points } });
|
try obj.properties.append(allocator, .{ .data = .{ .points = points } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Локальные границы ломаной: AABB по всем точкам.
|
/// Локальные границы: AABB по точкам.
|
||||||
pub fn getLocalBounds(obj: *const Object) !Rect_f {
|
pub fn getLocalBounds(obj: *const Object) !Rect_f {
|
||||||
try shape_mod.ensure(obj, .broken);
|
try shape_mod.ensure(obj, .broken);
|
||||||
const p = obj.getProperty(.points).?;
|
const p = obj.getProperty(.points).?;
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ const PropertyData = @import("../Property.zig").Data;
|
|||||||
const Rect_f = @import("../basic_models.zig").Rect_f;
|
const Rect_f = @import("../basic_models.zig").Rect_f;
|
||||||
const shape_mod = @import("shape.zig");
|
const shape_mod = @import("shape.zig");
|
||||||
|
|
||||||
/// Свойства фигуры по умолчанию (для создания и проверки типа). Теги для ensure выводятся отсюда.
|
/// Свойства фигуры по умолчанию.
|
||||||
pub const default_shape_properties = [_]Property{
|
pub const default_shape_properties = [_]Property{
|
||||||
.{ .data = .{ .radii = .{ .x = 50, .y = 50 } } },
|
.{ .data = .{ .radii = .{ .x = 50, .y = 50 } } },
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Теги обязательных свойств (выводятся из default_shape_properties).
|
/// Теги обязательных свойств.
|
||||||
pub fn getRequiredTags() []const std.meta.Tag(PropertyData) {
|
pub fn getRequiredTags() []const std.meta.Tag(PropertyData) {
|
||||||
return &([_]std.meta.Tag(PropertyData){std.meta.activeTag(default_shape_properties[0].data)});
|
return &([_]std.meta.Tag(PropertyData){std.meta.activeTag(default_shape_properties[0].data)});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ const PropertyData = @import("../Property.zig").Data;
|
|||||||
const Rect_f = @import("../basic_models.zig").Rect_f;
|
const Rect_f = @import("../basic_models.zig").Rect_f;
|
||||||
const shape_mod = @import("shape.zig");
|
const shape_mod = @import("shape.zig");
|
||||||
|
|
||||||
/// Свойства фигуры по умолчанию (для создания и проверки типа). Теги для ensure выводятся отсюда.
|
/// Свойства фигуры по умолчанию.
|
||||||
pub const default_shape_properties = [_]Property{
|
pub const default_shape_properties = [_]Property{
|
||||||
.{ .data = .{ .end_point = .{ .x = 100, .y = 0 } } },
|
.{ .data = .{ .end_point = .{ .x = 100, .y = 0 } } },
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Теги обязательных свойств (выводятся из default_shape_properties).
|
/// Теги обязательных свойств.
|
||||||
pub fn getRequiredTags() []const std.meta.Tag(PropertyData) {
|
pub fn getRequiredTags() []const std.meta.Tag(PropertyData) {
|
||||||
return &([_]std.meta.Tag(PropertyData){std.meta.activeTag(default_shape_properties[0].data)});
|
return &([_]std.meta.Tag(PropertyData){std.meta.activeTag(default_shape_properties[0].data)});
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object)
|
|||||||
for (default_shape_properties) |prop| try obj.properties.append(allocator, prop);
|
for (default_shape_properties) |prop| try obj.properties.append(allocator, prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Локальные границы линии: от (0,0) до end_point.
|
/// Локальные границы: от (0,0) до end_point.
|
||||||
pub fn getLocalBounds(obj: *const Object) !Rect_f {
|
pub fn getLocalBounds(obj: *const Object) !Rect_f {
|
||||||
try shape_mod.ensure(obj, .line);
|
try shape_mod.ensure(obj, .line);
|
||||||
const ep = obj.getProperty(.end_point).?;
|
const ep = obj.getProperty(.end_point).?;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const arc = @import("arc.zig");
|
|||||||
|
|
||||||
pub const Rect = basic_models.Rectf;
|
pub const Rect = basic_models.Rectf;
|
||||||
|
|
||||||
/// Создаёт объект с общими свойствами по умолчанию и специфичными для типа фигуры.
|
/// Создаёт объект с дефолтными общими и фигурными свойствами.
|
||||||
pub fn createObject(allocator: std.mem.Allocator, shape_kind: Object.ShapeKind) !Object {
|
pub fn createObject(allocator: std.mem.Allocator, shape_kind: Object.ShapeKind) !Object {
|
||||||
var obj = try createWithCommonProperties(allocator, shape_kind);
|
var obj = try createWithCommonProperties(allocator, shape_kind);
|
||||||
errdefer obj.deinit(allocator);
|
errdefer obj.deinit(allocator);
|
||||||
@@ -35,7 +35,7 @@ fn createWithCommonProperties(allocator: std.mem.Allocator, shape_kind: Object.S
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Проверяет, что объект имеет ожидаемый тип и все требуемые для этого типа свойства.
|
/// Проверяет тип объекта и наличие обязательных свойств.
|
||||||
pub fn ensure(obj: *const Object, expected_kind: Object.ShapeKind) !void {
|
pub fn ensure(obj: *const Object, expected_kind: Object.ShapeKind) !void {
|
||||||
if (obj.shape != expected_kind) return error.WrongShapeKind;
|
if (obj.shape != expected_kind) return error.WrongShapeKind;
|
||||||
const tags = requiredTagsFor(expected_kind);
|
const tags = requiredTagsFor(expected_kind);
|
||||||
@@ -53,7 +53,7 @@ fn requiredTagsFor(kind: Object.ShapeKind) []const std.meta.Tag(PropertyData) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Локальные границы объекта (AABB в своих координатах).
|
/// Локальные границы (AABB).
|
||||||
pub fn getLocalBounds(obj: *const Object) !Rect {
|
pub fn getLocalBounds(obj: *const Object) !Rect {
|
||||||
return switch (obj.shape) {
|
return switch (obj.shape) {
|
||||||
.line => line.getLocalBounds(obj),
|
.line => line.getLocalBounds(obj),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Document = @import("Document.zig");
|
const Document = @import("models/Document.zig");
|
||||||
const Object = Document.Object;
|
const Object = Document.Object;
|
||||||
const shape = @import("shape/shape.zig");
|
const shape = @import("models/shape/shape.zig");
|
||||||
const basic_models = @import("basic_models.zig");
|
const basic_models = @import("models/basic_models.zig");
|
||||||
const Size_f = basic_models.Size_f;
|
const Size_f = basic_models.Size_f;
|
||||||
const Point2_f = basic_models.Point2_f;
|
const Point2_f = basic_models.Point2_f;
|
||||||
const Scale2_f = basic_models.Scale2_f;
|
const Scale2_f = basic_models.Scale2_f;
|
||||||
@@ -17,7 +17,7 @@ fn randRgba(rng: std.Random) u32 {
|
|||||||
const g = rng.int(u8);
|
const g = rng.int(u8);
|
||||||
const b = rng.int(u8);
|
const b = rng.int(u8);
|
||||||
const a: u8 = @intCast(rng.intRangeLessThan(usize, 128, 256));
|
const a: u8 = @intCast(rng.intRangeLessThan(usize, 128, 256));
|
||||||
return r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24);
|
return (@as(u32, r) << 24) | (@as(u32, g) << 16) | (@as(u32, b) << 8) | a;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn randomShapeKind(rng: std.Random) Object.ShapeKind {
|
fn randomShapeKind(rng: std.Random) Object.ShapeKind {
|
||||||
@@ -25,7 +25,6 @@ fn randomShapeKind(rng: std.Random) Object.ShapeKind {
|
|||||||
return shapes_implemented[rng.intRangeLessThan(usize, 0, shapes_implemented.len)];
|
return shapes_implemented[rng.intRangeLessThan(usize, 0, shapes_implemented.len)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Случайно заполняет все доступные свойства объекта; позиция — в пределах документа.
|
|
||||||
fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size_f, obj: *Object, rng: std.Random) !void {
|
fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size_f, obj: *Object, rng: std.Random) !void {
|
||||||
const margin: f32 = 8;
|
const margin: f32 = 8;
|
||||||
const max_x = @max(0, doc_size.w - margin);
|
const max_x = @max(0, doc_size.w - margin);
|
||||||
@@ -51,7 +50,7 @@ fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size
|
|||||||
const stroke = randRgba(rng);
|
const stroke = randRgba(rng);
|
||||||
try obj.setProperty(allocator, .{ .data = .{ .stroke_rgba = stroke } });
|
try obj.setProperty(allocator, .{ .data = .{ .stroke_rgba = stroke } });
|
||||||
obj.setProperty(allocator, .{ .data = .{ .fill_rgba = randRgba(rng) } }) catch {};
|
obj.setProperty(allocator, .{ .data = .{ .fill_rgba = randRgba(rng) } }) catch {};
|
||||||
const thickness = randFloat(rng, max_x * 0.01, max_x * 0.1);
|
const thickness = randFloat(rng, max_x * 0.001, max_x * 0.01);
|
||||||
try obj.setProperty(allocator, .{ .data = .{ .thickness = thickness } });
|
try obj.setProperty(allocator, .{ .data = .{ .thickness = thickness } });
|
||||||
|
|
||||||
switch (obj.shape) {
|
switch (obj.shape) {
|
||||||
@@ -90,9 +89,7 @@ fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Создаёт в документе случайное количество фигур (в т.ч. вложенных).
|
/// Создаёт в документе случайные фигуры (line, ellipse, broken).
|
||||||
/// У каждой фигуры все доступные свойства задаются случайно; позиция — в пределах документа.
|
|
||||||
/// Реализованные типы: line, ellipse, broken.
|
|
||||||
pub fn addRandomShapes(doc: *Document, rng: std.Random) !void {
|
pub fn addRandomShapes(doc: *Document, rng: std.Random) !void {
|
||||||
const max_total: usize = 80;
|
const max_total: usize = 80;
|
||||||
var total_count: usize = 0;
|
var total_count: usize = 0;
|
||||||
@@ -174,7 +174,7 @@ pub fn renderEngine(self: *CpuRenderEngine) RenderEngine {
|
|||||||
return .{ .cpu = self };
|
return .{ .cpu = self };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Растеризует документ: фон + рекурсивная отрисовка фигур через конвейер (трансформ, прозрачность, наложение).
|
/// Растеризует документ в текстуру (фон + фигуры через конвейер).
|
||||||
pub fn renderDocument(self: *CpuRenderEngine, document: *const Document, canvas_size: Size_i, visible_rect: Rect_i) !?dvui.Texture {
|
pub fn renderDocument(self: *CpuRenderEngine, document: *const Document, canvas_size: Size_i, visible_rect: Rect_i) !?dvui.Texture {
|
||||||
const width = visible_rect.w;
|
const width = visible_rect.w;
|
||||||
const height = visible_rect.h;
|
const height = visible_rect.h;
|
||||||
@@ -182,7 +182,7 @@ pub fn renderDocument(self: *CpuRenderEngine, document: *const Document, canvas_
|
|||||||
defer self._allocator.free(pixels);
|
defer self._allocator.free(pixels);
|
||||||
|
|
||||||
for (pixels) |*p| p.* = .{ .r = 255, .g = 255, .b = 255, .a = 255 };
|
for (pixels) |*p| p.* = .{ .r = 255, .g = 255, .b = 255, .a = 255 };
|
||||||
cpu_draw.drawDocument(pixels, width, height, visible_rect, document, canvas_size);
|
try cpu_draw.drawDocument(pixels, width, height, visible_rect, document, canvas_size, self._allocator);
|
||||||
|
|
||||||
return try dvui.textureCreate(pixels, width, height, .nearest);
|
return try dvui.textureCreate(pixels, width, height, .nearest);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub const RenderEngine = union(enum) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Растеризует документ в текстуру (размер и видимая область в пикселях холста).
|
/// Растеризует документ в текстуру.
|
||||||
pub fn render(self: RenderEngine, document: *const Document, canvas_size: basic_models.Size_i, visible_rect: basic_models.Rect_i) !?dvui.Texture {
|
pub fn render(self: RenderEngine, document: *const Document, canvas_size: basic_models.Size_i, visible_rect: basic_models.Rect_i) !?dvui.Texture {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.cpu => |cpu_r| cpu_r.renderDocument(document, canvas_size, visible_rect),
|
.cpu => |cpu_r| cpu_r.renderDocument(document, canvas_size, visible_rect),
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ const DrawContext = pipeline.DrawContext;
|
|||||||
|
|
||||||
const Object = Document.Object;
|
const Object = Document.Object;
|
||||||
|
|
||||||
/// Рисует дугу (заглушка: пока не реализовано).
|
/// Дуга (не реализовано).
|
||||||
pub fn draw(_: *DrawContext, _: *const Object) void {}
|
pub fn draw(_: *DrawContext, _: *const Object) void {}
|
||||||
|
|||||||
@@ -9,15 +9,30 @@ 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;
|
||||||
|
|
||||||
/// Рисует ломаную по точкам в локальных координатах. Обводка по stroke_rgba.
|
/// Ломаная по точкам, обводка stroke_rgba
|
||||||
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
|
pub fn draw(
|
||||||
|
ctx: *DrawContext,
|
||||||
|
obj: *const Object,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
) !void {
|
||||||
const p_prop = obj.getProperty(.points) orelse return;
|
const p_prop = obj.getProperty(.points) orelse return;
|
||||||
const pts = p_prop.points.items;
|
const pts = p_prop.points.items;
|
||||||
if (pts.len < 2) return;
|
if (pts.len < 2) 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 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 copy_ctx = ctx.*;
|
||||||
|
copy_ctx.pixels = buffer;
|
||||||
|
copy_ctx.replace_mode = true;
|
||||||
|
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
while (i + 1 < pts.len) : (i += 1) {
|
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, thickness);
|
line.drawLine(©_ctx, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, stroke, thickness);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.compositeDrawerContext(©_ctx, copy_ctx.transform.opacity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ fn isVisible(obj: *const Object) bool {
|
|||||||
return if (obj.getProperty(.visible)) |p| p.visible else true;
|
return if (obj.getProperty(.visible)) |p| p.visible else true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drawObject(ctx: *DrawContext, obj: *const Object, parent_transform: Transform) void {
|
fn drawObject(
|
||||||
|
ctx: *DrawContext,
|
||||||
|
obj: *const Object,
|
||||||
|
parent_transform: Transform,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
) !void {
|
||||||
if (!isVisible(obj)) return;
|
if (!isVisible(obj)) return;
|
||||||
const local = getLocalTransform(obj);
|
const local = getLocalTransform(obj);
|
||||||
const world = Transform.compose(parent_transform, local);
|
const world = Transform.compose(parent_transform, local);
|
||||||
@@ -39,16 +44,17 @@ fn drawObject(ctx: *DrawContext, obj: *const Object, parent_transform: Transform
|
|||||||
switch (obj.shape) {
|
switch (obj.shape) {
|
||||||
.line => line.draw(ctx, obj),
|
.line => line.draw(ctx, obj),
|
||||||
.ellipse => ellipse.draw(ctx, obj),
|
.ellipse => ellipse.draw(ctx, obj),
|
||||||
.broken => broken.draw(ctx, obj),
|
.broken => try broken.draw(ctx, obj, allocator),
|
||||||
.arc => arc.draw(ctx, obj),
|
.arc => arc.draw(ctx, obj),
|
||||||
}
|
}
|
||||||
|
|
||||||
for (obj.children.items) |*child| {
|
for (obj.children.items) |*child| {
|
||||||
drawObject(ctx, child, world);
|
try drawObject(ctx, child, world, allocator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Рекурсивно рисует документ в буфер: сначала корневые объекты по порядку, затем их потомков (каждый следующий поверх предыдущего).
|
/// Рекурсивно рисует документ в буфер (объекты и потомки по порядку).
|
||||||
|
/// allocator опционален; если передан, ломаные рисуются через слой (без двойного наложения при alpha < 1).
|
||||||
pub fn drawDocument(
|
pub fn drawDocument(
|
||||||
pixels: []@import("dvui").Color.PMA,
|
pixels: []@import("dvui").Color.PMA,
|
||||||
buf_width: u32,
|
buf_width: u32,
|
||||||
@@ -56,7 +62,8 @@ pub fn drawDocument(
|
|||||||
visible_rect: Rect_i,
|
visible_rect: Rect_i,
|
||||||
document: *const Document,
|
document: *const Document,
|
||||||
canvas_size: Size_i,
|
canvas_size: Size_i,
|
||||||
) void {
|
allocator: std.mem.Allocator,
|
||||||
|
) !void {
|
||||||
const scale_x: f32 = if (document.size.w > 0) @as(f32, @floatFromInt(canvas_size.w)) / document.size.w else 0;
|
const scale_x: f32 = if (document.size.w > 0) @as(f32, @floatFromInt(canvas_size.w)) / document.size.w else 0;
|
||||||
const scale_y: f32 = if (document.size.h > 0) @as(f32, @floatFromInt(canvas_size.h)) / document.size.h else 0;
|
const scale_y: f32 = if (document.size.h > 0) @as(f32, @floatFromInt(canvas_size.h)) / document.size.h else 0;
|
||||||
|
|
||||||
@@ -68,10 +75,8 @@ pub fn drawDocument(
|
|||||||
.scale_x = scale_x,
|
.scale_x = scale_x,
|
||||||
.scale_y = scale_y,
|
.scale_y = scale_y,
|
||||||
};
|
};
|
||||||
// вывести visible_rect
|
|
||||||
std.debug.print("visible_rect: {{ x: {}, y: {}, w: {}, h: {} }}\n", .{ visible_rect.x, visible_rect.y, visible_rect.w, visible_rect.h });
|
|
||||||
const identity = Transform{};
|
const identity = Transform{};
|
||||||
for (document.objects.items) |*obj| {
|
for (document.objects.items) |*obj| {
|
||||||
drawObject(&ctx, obj, identity);
|
try drawObject(&ctx, obj, identity, allocator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,24 @@ const Color = @import("dvui").Color;
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
/// Рисует эллипс: центр (0,0), полуоси radii. Растеризация в координатах буфера (обводка кольцом).
|
/// Эллипс с центром (0,0) и полуосями radii (обводка с учётом thickness).
|
||||||
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
|
pub fn draw(ctx: *DrawContext, obj: *const Object) 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;
|
||||||
|
|
||||||
|
// Полуширина обводки в нормализованных единицах (d = (x/rx)² + (y/ry)², граница при d=1).
|
||||||
|
const min_r = @min(rx, ry);
|
||||||
|
const half_norm = thickness / (2.0 * min_r);
|
||||||
|
const inner = @max(0.0, 1.0 - half_norm);
|
||||||
|
const outer = 1.0 + half_norm;
|
||||||
|
const d_inner_sq = inner * inner;
|
||||||
|
const d_outer_sq = outer * outer;
|
||||||
|
|
||||||
const corners = [_]struct { x: f32, y: f32 }{
|
const corners = [_]struct { x: f32, y: f32 }{
|
||||||
.{ .x = -rx, .y = -ry },
|
.{ .x = -rx, .y = -ry },
|
||||||
@@ -42,16 +52,43 @@ pub fn draw(ctx: *DrawContext, obj: *const Object) void {
|
|||||||
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);
|
||||||
|
|
||||||
|
// Один раз считаем аффин 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_ry = 1.0 / ry;
|
||||||
|
|
||||||
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 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 w = ctx.bufferToWorld(@as(f32, @floatFromInt(bx)) + 0.5, @as(f32, @floatFromInt(by)) + 0.5);
|
const buf_x = @as(f32, @floatFromInt(bx)) + 0.5;
|
||||||
const loc = ctx.worldToLocal(w.x, w.y);
|
const loc_x = buf_x * m00 + row_loc_x_off;
|
||||||
const nx = loc.x / rx;
|
const loc_y = buf_x * m10 + row_loc_y_off;
|
||||||
const ny = loc.y / ry;
|
const nx = loc_x * inv_rx;
|
||||||
|
const ny = loc_y * inv_ry;
|
||||||
const d = nx * nx + ny * ny;
|
const d = nx * nx + ny * ny;
|
||||||
if (d >= 0.9 and d <= 1.1) {
|
if (d >= d_inner_sq and d <= d_outer_sq) {
|
||||||
ctx.blendPixelAtBuffer(@intCast(bx), @intCast(by), stroke);
|
ctx.blendPixelAtBuffer(@intCast(bx), @intCast(by), stroke);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const std = @import("std");
|
||||||
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;
|
||||||
@@ -7,7 +8,7 @@ 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) до end_point. Растеризация в координатах буфера (без пробелов при зуме).
|
/// Линия от (0,0) до end_point.
|
||||||
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
|
pub fn draw(ctx: *DrawContext, obj: *const Object) void {
|
||||||
const ep_prop = obj.getProperty(.end_point) orelse return;
|
const ep_prop = obj.getProperty(.end_point) orelse return;
|
||||||
const end_x = ep_prop.end_point.x;
|
const end_x = ep_prop.end_point.x;
|
||||||
@@ -17,37 +18,90 @@ pub fn draw(ctx: *DrawContext, obj: *const Object) void {
|
|||||||
drawLine(ctx, 0, 0, end_x, end_y, stroke, thickness);
|
drawLine(ctx, 0, 0, end_x, end_y, stroke, thickness);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Линия по локальным координатам фигуры: переводит концы в буфер и рисует в пикселях буфера.
|
/// Рисует отрезок по локальным концам (перевод в буфер внутри).
|
||||||
pub fn drawLine(ctx: *DrawContext, x0: f32, y0: f32, x1: f32, y1: f32, color: Color.PMA, thickness: f32) void {
|
pub fn drawLine(ctx: *DrawContext, x0: f32, y0: f32, x1: f32, y1: f32, color: Color.PMA, thickness: f32) void {
|
||||||
const w0 = ctx.localToWorld(x0, y0);
|
const w0 = ctx.localToWorld(x0, y0);
|
||||||
const w1 = ctx.localToWorld(x1, y1);
|
const w1 = ctx.localToWorld(x1, y1);
|
||||||
const b0 = ctx.worldToBuffer(w0.x, w0.y);
|
const b0 = ctx.worldToBuffer(w0.x, w0.y);
|
||||||
const b1 = ctx.worldToBuffer(w1.x, w1.y);
|
const b1 = ctx.worldToBuffer(w1.x, w1.y);
|
||||||
drawLineInBuffer(ctx, b0.x, b0.y, b1.x, b1.y, color, thickness);
|
const t = &ctx.transform;
|
||||||
|
const scale = @sqrt(t.scale.scale_x * ctx.scale_x * t.scale.scale_y * ctx.scale_y);
|
||||||
|
const thickness_px: u32 = @max(@as(u32, 1), @as(u32, @intFromFloat(std.math.round(thickness * scale))));
|
||||||
|
drawLineInBuffer(ctx, b0.x, b0.y, b1.x, b1.y, color, thickness_px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Брезенхем в координатах буфера; пиксели вне [0, buf_width) x [0, buf_height) пропускаются.
|
/// Точка (px, py) лежит строго внутри круга с центром (cx, cy) и радиусом в квадрате r_sq (граница не включается).
|
||||||
fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, color: Color.PMA, thickness: f32) void {
|
fn insideCircle(px: i32, py: i32, cx: i32, cy: i32, r_sq: i32) bool {
|
||||||
|
const dx = px - cx;
|
||||||
|
const dy = py - cy;
|
||||||
|
return dx * dx + dy * dy < r_sq;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Заливает круг в буфере (для скруглённых концов отрезка).
|
||||||
|
fn fillCircleAtBuffer(ctx: *DrawContext, cx: i32, cy: i32, radius: i32, color: Color.PMA) void {
|
||||||
|
const bw: i32 = @intCast(ctx.buf_width);
|
||||||
|
const bh: i32 = @intCast(ctx.buf_height);
|
||||||
|
const r_sq = radius * radius;
|
||||||
|
var dy: i32 = -radius;
|
||||||
|
while (dy <= radius) : (dy += 1) {
|
||||||
|
var dx: i32 = -radius;
|
||||||
|
while (dx <= radius) : (dx += 1) {
|
||||||
|
const px = cx + dx;
|
||||||
|
const py = cy + dy;
|
||||||
|
if (!insideCircle(px, py, cx, cy, r_sq)) continue;
|
||||||
|
if (px >= 0 and px < bw and py >= 0 and py < bh) {
|
||||||
|
ctx.blendPixelAtBuffer(@intCast(px), @intCast(py), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, color: Color.PMA, thickness_px: u32) void {
|
||||||
const bw = ctx.buf_width;
|
const bw = ctx.buf_width;
|
||||||
const bh = ctx.buf_height;
|
const bh = ctx.buf_height;
|
||||||
const dx: i32 = @intCast(@abs(bx1 - bx0));
|
const dx: i32 = @intCast(@abs(bx1 - bx0));
|
||||||
const dy: i32 = -@as(i32, @intCast(@abs(by1 - by0)));
|
const dy: i32 = @intCast(@abs(by1 - by0));
|
||||||
const sx: i32 = if (bx0 < bx1) 1 else -1;
|
const sx: i32 = if (bx0 < bx1) 1 else -1;
|
||||||
const sy: i32 = if (by0 < by1) 1 else -1;
|
const sy: i32 = if (by0 < by1) 1 else -1;
|
||||||
var err = dx + dy;
|
var err = dx - dy;
|
||||||
var x = bx0;
|
var x = bx0;
|
||||||
var y = by0;
|
var y = by0;
|
||||||
|
const half: i32 = @intCast(thickness_px / 2);
|
||||||
|
const half_sq = half * half;
|
||||||
|
const thickness_i: i32 = @intCast(thickness_px);
|
||||||
|
|
||||||
_ = thickness;
|
// Брезенхем + штамп по доминирующей оси: горизонтальная линия — вертикальный штамп, вертикальная — горизонтальный.
|
||||||
|
const more_horizontal = dx >= dy;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (x >= 0 and x < bw and y >= 0 and y < bh) {
|
const near_start = @abs(x - bx0) + @abs(y - by0) <= thickness_i;
|
||||||
ctx.blendPixelAtBuffer(@intCast(x), @intCast(y), color);
|
const near_end = @abs(x - bx1) + @abs(y - by1) <= thickness_i;
|
||||||
|
|
||||||
|
if (more_horizontal) {
|
||||||
|
var yo: i32 = -half;
|
||||||
|
while (yo <= half) : (yo += 1) {
|
||||||
|
const py = y + yo;
|
||||||
|
if (near_start and insideCircle(x, py, bx0, by0, half_sq)) continue;
|
||||||
|
if (near_end and insideCircle(x, py, bx1, by1, half_sq)) continue;
|
||||||
|
if (x >= 0 and x < @as(i32, @intCast(bw)) and py >= 0 and py < @as(i32, @intCast(bh))) {
|
||||||
|
ctx.blendPixelAtBuffer(@intCast(x), @intCast(py), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var xo: i32 = -half;
|
||||||
|
while (xo <= half) : (xo += 1) {
|
||||||
|
const px = x + xo;
|
||||||
|
if (near_start and insideCircle(px, y, bx0, by0, half_sq)) continue;
|
||||||
|
if (near_end and insideCircle(px, y, bx1, by1, half_sq)) continue;
|
||||||
|
if (px >= 0 and px < @as(i32, @intCast(bw)) and y >= 0 and y < @as(i32, @intCast(bh))) {
|
||||||
|
ctx.blendPixelAtBuffer(@intCast(px), @intCast(y), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (x == bx1 and y == by1) break;
|
if (x == bx1 and y == by1) break;
|
||||||
const e2 = 2 * err;
|
const e2 = 2 * err;
|
||||||
if (e2 >= dy) {
|
if (e2 >= -dy) {
|
||||||
err += dy;
|
err -= dy;
|
||||||
x += sx;
|
x += sx;
|
||||||
}
|
}
|
||||||
if (e2 <= dx) {
|
if (e2 <= dx) {
|
||||||
@@ -55,4 +109,10 @@ fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, c
|
|||||||
y += sy;
|
y += sy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Скруглённые концы: круги в крайних точках.
|
||||||
|
if (half > 0) {
|
||||||
|
fillCircleAtBuffer(ctx, bx0, by0, half, color);
|
||||||
|
fillCircleAtBuffer(ctx, bx1, by1, half, color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ const Scale2_f = basic_models.Scale2_f;
|
|||||||
const Rect_i = basic_models.Rect_i;
|
const Rect_i = basic_models.Rect_i;
|
||||||
const Color = dvui.Color;
|
const Color = dvui.Color;
|
||||||
|
|
||||||
/// Трансформ объекта в мировых координатах документа (позиция, угол, масштаб, непрозрачность).
|
/// Трансформ объекта: позиция, угол, масштаб, непрозрачность.
|
||||||
pub const Transform = struct {
|
pub const Transform = struct {
|
||||||
position: Point2_f = .{},
|
position: Point2_f = .{},
|
||||||
angle: f32 = 0,
|
angle: f32 = 0,
|
||||||
scale: Scale2_f = .{},
|
scale: Scale2_f = .{},
|
||||||
opacity: f32 = 1.0,
|
opacity: f32 = 1.0,
|
||||||
|
|
||||||
/// Композиция: мировой трансформ = parent * local (local в пространстве родителя).
|
/// Композиция: world = parent * local.
|
||||||
pub fn compose(parent: Transform, local: Transform) Transform {
|
pub fn compose(parent: Transform, local: Transform) Transform {
|
||||||
const cos_a = std.math.cos(parent.angle);
|
const cos_a = std.math.cos(parent.angle);
|
||||||
const sin_a = std.math.sin(parent.angle);
|
const sin_a = std.math.sin(parent.angle);
|
||||||
@@ -36,8 +36,7 @@ pub const Transform = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Единый конвейер: принимает позицию в локальных координатах фигуры и цвет пикселя,
|
/// Конвейер отрисовки: локальные координаты -> трансформ -> буфер.
|
||||||
/// применяет трансформ (вращение, масштаб, перенос) и непрозрачность, накладывает на буфер.
|
|
||||||
pub const DrawContext = struct {
|
pub const DrawContext = struct {
|
||||||
pixels: []Color.PMA,
|
pixels: []Color.PMA,
|
||||||
buf_width: u32,
|
buf_width: u32,
|
||||||
@@ -46,12 +45,14 @@ pub const DrawContext = struct {
|
|||||||
scale_x: f32,
|
scale_x: f32,
|
||||||
scale_y: f32,
|
scale_y: f32,
|
||||||
transform: Transform = .{},
|
transform: Transform = .{},
|
||||||
|
/// Если true, blendPixelAtBuffer перезаписывает пиксель без бленда
|
||||||
|
replace_mode: bool = false,
|
||||||
|
|
||||||
pub fn setTransform(self: *DrawContext, t: Transform) void {
|
pub fn setTransform(self: *DrawContext, t: Transform) void {
|
||||||
self.transform = t;
|
self.transform = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Локальные координаты фигуры -> мировые (документ).
|
/// Локальные -> мировые.
|
||||||
pub fn localToWorld(self: *const DrawContext, local_x: f32, local_y: f32) Point2_f {
|
pub fn localToWorld(self: *const DrawContext, local_x: f32, local_y: f32) Point2_f {
|
||||||
const t = &self.transform;
|
const t = &self.transform;
|
||||||
const cos_a = std.math.cos(t.angle);
|
const cos_a = std.math.cos(t.angle);
|
||||||
@@ -62,7 +63,7 @@ pub const DrawContext = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Мировые координаты документа -> координаты в буфере (float; могут быть вне [0, buf_w] x [0, buf_h]).
|
/// Мировые -> буфер (float).
|
||||||
pub fn worldToBufferF(self: *const DrawContext, wx: f32, wy: f32) Point2_f {
|
pub fn worldToBufferF(self: *const DrawContext, wx: f32, wy: f32) Point2_f {
|
||||||
const canvas_x = wx * self.scale_x;
|
const canvas_x = wx * self.scale_x;
|
||||||
const canvas_y = wy * self.scale_y;
|
const canvas_y = wy * self.scale_y;
|
||||||
@@ -74,7 +75,7 @@ pub const DrawContext = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Мировые координаты документа -> целочисленные координаты в буфере (округление до ближайшего пикселя).
|
/// Мировые -> буфер (целые).
|
||||||
pub fn worldToBuffer(self: *const DrawContext, wx: f32, wy: f32) Point2_i {
|
pub fn worldToBuffer(self: *const DrawContext, wx: f32, wy: f32) Point2_i {
|
||||||
const b = self.worldToBufferF(wx, wy);
|
const b = self.worldToBufferF(wx, wy);
|
||||||
return .{
|
return .{
|
||||||
@@ -83,7 +84,7 @@ pub const DrawContext = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Координаты буфера -> мировые (документ). scale_x/scale_y не должны быть 0.
|
/// Буфер -> мировые.
|
||||||
pub fn bufferToWorld(self: *const DrawContext, buf_x: f32, buf_y: f32) Point2_f {
|
pub fn bufferToWorld(self: *const DrawContext, buf_x: f32, buf_y: f32) Point2_f {
|
||||||
const vx = @as(f32, @floatFromInt(self.visible_rect.x));
|
const vx = @as(f32, @floatFromInt(self.visible_rect.x));
|
||||||
const vy = @as(f32, @floatFromInt(self.visible_rect.y));
|
const vy = @as(f32, @floatFromInt(self.visible_rect.y));
|
||||||
@@ -97,7 +98,7 @@ pub const DrawContext = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Мировые координаты -> локальные фигуры (обратное к localToWorld).
|
/// Мировые -> локальные.
|
||||||
pub fn worldToLocal(self: *const DrawContext, wx: f32, wy: f32) Point2_f {
|
pub fn worldToLocal(self: *const DrawContext, wx: f32, wy: f32) Point2_f {
|
||||||
const t = &self.transform;
|
const t = &self.transform;
|
||||||
const dx = wx - t.position.x;
|
const dx = wx - t.position.x;
|
||||||
@@ -112,16 +113,20 @@ pub const DrawContext = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Смешивает цвет в пикселе буфера (bx, by) с учётом opacity текущего трансформа. Bounds не проверяются.
|
/// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель.
|
||||||
pub fn blendPixelAtBuffer(self: *DrawContext, bx: u32, by: u32, color: Color.PMA) void {
|
pub fn blendPixelAtBuffer(self: *DrawContext, bx: u32, by: u32, color: Color.PMA) void {
|
||||||
if (bx >= self.buf_width or by >= self.buf_height) return;
|
if (bx >= self.buf_width or by >= self.buf_height) return;
|
||||||
const t = &self.transform;
|
|
||||||
const idx = by * self.buf_width + bx;
|
const idx = by * self.buf_width + bx;
|
||||||
const dst = &self.pixels[idx];
|
const dst = &self.pixels[idx];
|
||||||
|
if (self.replace_mode) {
|
||||||
|
dst.* = color;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const t = &self.transform;
|
||||||
const a = @as(f32, @floatFromInt(color.a)) / 255.0 * t.opacity;
|
const a = @as(f32, @floatFromInt(color.a)) / 255.0 * t.opacity;
|
||||||
const src_r = @as(f32, @floatFromInt(color.r)) * a;
|
const src_r = @as(f32, @floatFromInt(color.r)) * t.opacity;
|
||||||
const src_g = @as(f32, @floatFromInt(color.g)) * a;
|
const src_g = @as(f32, @floatFromInt(color.g)) * t.opacity;
|
||||||
const src_b = @as(f32, @floatFromInt(color.b)) * a;
|
const src_b = @as(f32, @floatFromInt(color.b)) * t.opacity;
|
||||||
const inv_a = 1.0 - 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.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.g = @intFromFloat(std.math.clamp(src_g + inv_a * @as(f32, @floatFromInt(dst.g)), 0, 255));
|
||||||
@@ -129,7 +134,27 @@ pub const DrawContext = struct {
|
|||||||
dst.a = @intFromFloat(std.math.clamp(a * 255 + inv_a * @as(f32, @floatFromInt(dst.a)), 0, 255));
|
dst.a = @intFromFloat(std.math.clamp(a * 255 + inv_a * @as(f32, @floatFromInt(dst.a)), 0, 255));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Записывает пиксель в локальных координатах фигуры с учётом трансформа и прозрачности (PMA blend).
|
/// Накладывает буфер другого контекста на этот с заданной прозрачностью (один бленд на пиксель). Размеры буферов должны совпадать.
|
||||||
|
pub fn compositeDrawerContext(self: *DrawContext, other: *const DrawContext, opacity: f32) void {
|
||||||
|
if (self.buf_width != other.buf_width or self.buf_height != other.buf_height) return;
|
||||||
|
const n = self.buf_width * self.buf_height;
|
||||||
|
for (0..n) |i| {
|
||||||
|
const src = other.pixels[i];
|
||||||
|
if (src.a == 0) continue;
|
||||||
|
const dst = &self.pixels[i];
|
||||||
|
const a = @as(f32, @floatFromInt(src.a)) / 255.0 * opacity;
|
||||||
|
const src_r = @as(f32, @floatFromInt(src.r)) * opacity;
|
||||||
|
const src_g = @as(f32, @floatFromInt(src.g)) * opacity;
|
||||||
|
const src_b = @as(f32, @floatFromInt(src.b)) * opacity;
|
||||||
|
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).
|
||||||
pub fn blendPixelLocal(self: *DrawContext, local_x: f32, local_y: f32, color: Color.PMA) void {
|
pub fn blendPixelLocal(self: *DrawContext, local_x: f32, local_y: f32, color: Color.PMA) void {
|
||||||
const w = self.localToWorld(local_x, local_y);
|
const w = self.localToWorld(local_x, local_y);
|
||||||
const b = self.worldToBufferF(w.x, w.y);
|
const b = self.worldToBufferF(w.x, w.y);
|
||||||
@@ -142,11 +167,12 @@ pub const DrawContext = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Конвертирует u32 0xRRGGBBAA в Color.PMA.
|
||||||
pub fn rgbaToPma(rgba: u32) Color.PMA {
|
pub fn rgbaToPma(rgba: u32) Color.PMA {
|
||||||
const r: u8 = @intCast((rgba >> 0) & 0xFF);
|
const r: u8 = @intCast((rgba >> 24) & 0xFF);
|
||||||
const g: u8 = @intCast((rgba >> 8) & 0xFF);
|
const g: u8 = @intCast((rgba >> 16) & 0xFF);
|
||||||
const b: u8 = @intCast((rgba >> 16) & 0xFF);
|
const b: u8 = @intCast((rgba >> 8) & 0xFF);
|
||||||
const a: u8 = @intCast((rgba >> 24) & 0xFF);
|
const a: u8 = @intCast((rgba >> 0) & 0xFF);
|
||||||
if (a == 0) return .{ .r = 0, .g = 0, .b = 0, .a = 0 };
|
if (a == 0) return .{ .r = 0, .g = 0, .b = 0, .a = 0 };
|
||||||
const af: f32 = @as(f32, @floatFromInt(a)) / 255.0;
|
const af: f32 = @as(f32, @floatFromInt(a)) / 255.0;
|
||||||
return .{
|
return .{
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
//! Общий интерфейс инструмента. Реализации живут в каталоге tools/.
|
|
||||||
|
|
||||||
const Point2_f = @import("models/basic_models.zig").Point2_f;
|
|
||||||
|
|
||||||
/// Контекст вызова: холст и точка в координатах документа.
|
|
||||||
pub const ToolContext = struct {
|
|
||||||
canvas: *anyopaque,
|
|
||||||
document_point: Point2_f,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Интерфейс инструмента: один метод — клик по холсту в позиции курсора.
|
|
||||||
pub const Tool = struct {
|
|
||||||
onClick: *const fn (*const ToolContext) void,
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
//! Список инструментов по умолчанию для тулбара. Реализации — в каталоге tools/.
|
|
||||||
|
|
||||||
const Toolbar = @import("Toolbar.zig");
|
const Toolbar = @import("Toolbar.zig");
|
||||||
const line = @import("tools/line.zig");
|
const line = @import("tools/line.zig");
|
||||||
|
const ellipse = @import("tools/ellipse.zig");
|
||||||
|
const arc = @import("tools/arc.zig");
|
||||||
|
const broken = @import("tools/broken.zig");
|
||||||
const icons = @import("icons.zig");
|
const icons = @import("icons.zig");
|
||||||
|
|
||||||
pub const default_tools = [_]Toolbar.ToolDescriptor{
|
pub const default_tools = [_]Toolbar.ToolDescriptor{
|
||||||
@@ -10,4 +11,19 @@ pub const default_tools = [_]Toolbar.ToolDescriptor{
|
|||||||
.icon_tvg = icons.line,
|
.icon_tvg = icons.line,
|
||||||
.implementation = &line.tool,
|
.implementation = &line.tool,
|
||||||
},
|
},
|
||||||
|
.{
|
||||||
|
.name = "Ellipse",
|
||||||
|
.icon_tvg = icons.ellipse,
|
||||||
|
.implementation = &ellipse.tool,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.name = "Arc",
|
||||||
|
.icon_tvg = icons.arc,
|
||||||
|
.implementation = &arc.tool,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.name = "Broken line",
|
||||||
|
.icon_tvg = icons.broken,
|
||||||
|
.implementation = &broken.tool,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
12
src/tools/arc.zig
Normal file
12
src/tools/arc.zig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const Tool = @import("../Tool.zig");
|
||||||
|
const shape = @import("../models/shape/shape.zig");
|
||||||
|
|
||||||
|
fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
|
||||||
|
const canvas = ctx.canvas;
|
||||||
|
var obj = shape.createObject(canvas.document.allocator, .arc) catch return;
|
||||||
|
defer obj.deinit(canvas.allocator);
|
||||||
|
try obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = ctx.document_point } });
|
||||||
|
try canvas.document.addObject(obj);
|
||||||
|
canvas.requestRedraw();
|
||||||
|
}
|
||||||
|
pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick };
|
||||||
12
src/tools/broken.zig
Normal file
12
src/tools/broken.zig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const Tool = @import("../Tool.zig");
|
||||||
|
const shape = @import("../models/shape/shape.zig");
|
||||||
|
|
||||||
|
fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
|
||||||
|
const canvas = ctx.canvas;
|
||||||
|
var obj = shape.createObject(canvas.document.allocator, .broken) catch return;
|
||||||
|
defer obj.deinit(canvas.allocator);
|
||||||
|
try obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = ctx.document_point } });
|
||||||
|
try canvas.document.addObject(obj);
|
||||||
|
canvas.requestRedraw();
|
||||||
|
}
|
||||||
|
pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick };
|
||||||
12
src/tools/ellipse.zig
Normal file
12
src/tools/ellipse.zig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const Tool = @import("../Tool.zig");
|
||||||
|
const shape = @import("../models/shape/shape.zig");
|
||||||
|
|
||||||
|
fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
|
||||||
|
const canvas = ctx.canvas;
|
||||||
|
var obj = shape.createObject(canvas.document.allocator, .ellipse) catch return;
|
||||||
|
defer obj.deinit(canvas.allocator);
|
||||||
|
try obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = ctx.document_point } });
|
||||||
|
try canvas.document.addObject(obj);
|
||||||
|
canvas.requestRedraw();
|
||||||
|
}
|
||||||
|
pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick };
|
||||||
@@ -1,21 +1,14 @@
|
|||||||
//! Инструмент «Линия»: создаёт линию в позиции клика.
|
const std = @import("std");
|
||||||
|
|
||||||
const Canvas = @import("../Canvas.zig");
|
const Canvas = @import("../Canvas.zig");
|
||||||
const tool_interface = @import("../tool_interface.zig");
|
const Tool = @import("../Tool.zig");
|
||||||
const shape = @import("../models/shape/shape.zig");
|
const shape = @import("../models/shape/shape.zig");
|
||||||
|
|
||||||
fn onClick(ctx: *const tool_interface.ToolContext) void {
|
fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
|
||||||
const canvas: *Canvas = @alignCast(@ptrCast(ctx.canvas));
|
const canvas = ctx.canvas;
|
||||||
var obj = shape.createObject(canvas.document.allocator, .line) catch return;
|
var obj = shape.createObject(canvas.document.allocator, .line) catch return;
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = ctx.document_point } }) catch {
|
defer obj.deinit(canvas.allocator);
|
||||||
obj.deinit(canvas.document.allocator);
|
try obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = ctx.document_point } });
|
||||||
return;
|
try canvas.document.addObject(obj);
|
||||||
};
|
|
||||||
canvas.document.addObject(obj) catch {
|
|
||||||
obj.deinit(canvas.document.allocator);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
|
pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick };
|
||||||
pub const tool = tool_interface.Tool{ .onClick = onClick };
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const dvui = @import("dvui");
|
|||||||
const dvui_ext = @import("dvui_ext.zig");
|
const dvui_ext = @import("dvui_ext.zig");
|
||||||
const Canvas = @import("../Canvas.zig");
|
const Canvas = @import("../Canvas.zig");
|
||||||
const Rect_i = @import("../models/basic_models.zig").Rect_i;
|
const Rect_i = @import("../models/basic_models.zig").Rect_i;
|
||||||
const tool_interface = @import("../tool_interface.zig");
|
const Tool = @import("../Tool.zig");
|
||||||
|
|
||||||
pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void {
|
pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void {
|
||||||
var textured = dvui_ext.texturedBox(content_rect_scale, dvui.Rect.all(20));
|
var textured = dvui_ext.texturedBox(content_rect_scale, dvui.Rect.all(20));
|
||||||
@@ -26,7 +26,25 @@ pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void {
|
|||||||
}
|
}
|
||||||
scroll.deinit();
|
scroll.deinit();
|
||||||
|
|
||||||
drawToolbar(canvas);
|
// Тулбар поверх scroll
|
||||||
|
var toolbar_box = dvui.box(
|
||||||
|
@src(),
|
||||||
|
.{ .dir = .horizontal },
|
||||||
|
.{
|
||||||
|
.expand = .none,
|
||||||
|
.background = false,
|
||||||
|
.gravity_x = 0.0,
|
||||||
|
.gravity_y = 0.0,
|
||||||
|
.margin = dvui.Rect{ .x = 8, .y = 8 },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
{
|
||||||
|
drawToolbar(canvas);
|
||||||
|
}
|
||||||
|
// Сохраняем rect тулбара для следующего кадра — в handleCanvasMouse исключаем из него клики
|
||||||
|
canvas.toolbar_rect_scale = toolbar_box.data().contentRectScale();
|
||||||
|
toolbar_box.deinit();
|
||||||
|
|
||||||
dvui.label(@src(), "Canvas", .{}, .{ .gravity_x = 0.5, .gravity_y = 0.0 });
|
dvui.label(@src(), "Canvas", .{}, .{ .gravity_x = 0.5, .gravity_y = 0.0 });
|
||||||
}
|
}
|
||||||
overlay.deinit();
|
overlay.deinit();
|
||||||
@@ -145,16 +163,25 @@ fn handleCanvasZoom(canvas: *Canvas, scroll: anytype) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handleCanvasMouse(canvas: *Canvas, scroll: anytype) void {
|
fn handleCanvasMouse(canvas: *Canvas, scroll: *dvui.ScrollAreaWidget) void {
|
||||||
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
const natural_scale = if (canvas.native_scaling) 1 else dvui.windowNaturalScale();
|
||||||
|
const scroll_data = scroll.data();
|
||||||
|
|
||||||
for (dvui.events()) |*e| {
|
for (dvui.events()) |*e| {
|
||||||
switch (e.evt) {
|
switch (e.evt) {
|
||||||
.mouse => |*mouse| {
|
.mouse => |*mouse| {
|
||||||
if (mouse.action != .press or mouse.button != .left) continue;
|
if (mouse.action != .press or mouse.button != .left) continue;
|
||||||
if (!dvui.eventMatchSimple(e, scroll.data())) continue;
|
if (e.handled) continue;
|
||||||
|
if (!dvui.eventMatchSimple(e, scroll_data)) continue;
|
||||||
|
|
||||||
const viewport_pt = scroll.data().contentRectScale().pointFromPhysical(mouse.p);
|
// Не обрабатывать клик, если он попал в область тулбара (rect с предыдущего кадра).
|
||||||
|
if (canvas.toolbar_rect_scale) |trs| {
|
||||||
|
const pt = trs.pointFromPhysical(mouse.p);
|
||||||
|
const r = trs.r;
|
||||||
|
if (pt.x >= 0 and pt.x * trs.s < r.w and pt.y >= 0 and pt.y * trs.s < r.h) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport_pt = scroll_data.contentRectScale().pointFromPhysical(mouse.p);
|
||||||
const content_pt = dvui.Point{
|
const content_pt = dvui.Point{
|
||||||
.x = viewport_pt.x + canvas.scroll.viewport.x,
|
.x = viewport_pt.x + canvas.scroll.viewport.x,
|
||||||
.y = viewport_pt.y + canvas.scroll.viewport.y,
|
.y = viewport_pt.y + canvas.scroll.viewport.y,
|
||||||
@@ -163,11 +190,13 @@ fn handleCanvasMouse(canvas: *Canvas, scroll: anytype) void {
|
|||||||
canvas.cursor_document_point = if (canvas.isContentPointOnDocument(content_pt, natural_scale)) doc_pt else null;
|
canvas.cursor_document_point = if (canvas.isContentPointOnDocument(content_pt, natural_scale)) doc_pt else null;
|
||||||
if (canvas.cursor_document_point) |point| {
|
if (canvas.cursor_document_point) |point| {
|
||||||
if (canvas.toolbar.currentDescriptor()) |desc| {
|
if (canvas.toolbar.currentDescriptor()) |desc| {
|
||||||
var ctx = tool_interface.ToolContext{
|
var ctx = Tool.ToolContext{
|
||||||
.canvas = canvas,
|
.canvas = canvas,
|
||||||
.document_point = point,
|
.document_point = point,
|
||||||
};
|
};
|
||||||
desc.implementation.onClick(&ctx);
|
desc.implementation.onCanvasClick(&ctx) catch |err| {
|
||||||
|
std.debug.print("onCanvasClick error: {}\n", .{err});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -194,16 +223,21 @@ fn drawToolbar(canvas: *Canvas) void {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
{
|
{
|
||||||
|
var to_select: ?usize = null;
|
||||||
for (tools_list, 0..) |*tool_desc, i| {
|
for (tools_list, 0..) |*tool_desc, i| {
|
||||||
const is_selected = (canvas.toolbar.selected_index == i);
|
const is_selected = canvas.toolbar.selected_index == i;
|
||||||
|
const selected_fill = dvui.themeGet().focus;
|
||||||
const opts: dvui.Options = .{
|
const opts: dvui.Options = .{
|
||||||
.id_extra = i,
|
.id_extra = i,
|
||||||
.color_fill = if (is_selected) dvui.Color.transparent else undefined,
|
.color_fill = if (is_selected) selected_fill else null,
|
||||||
};
|
};
|
||||||
if (dvui.buttonIcon(@src(), tool_desc.name, tool_desc.icon_tvg, .{}, .{}, opts)) {
|
if (dvui.buttonIcon(@src(), tool_desc.name, tool_desc.icon_tvg, .{}, .{}, opts)) {
|
||||||
canvas.toolbar.select(i);
|
to_select = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (to_select) |index| {
|
||||||
|
canvas.toolbar.select(index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bar.deinit();
|
bar.deinit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ pub fn leftPanel(ctx: *WindowContext) void {
|
|||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (dvui.button(@src(), "Add random shapes", .{}, .{})) {
|
||||||
|
canvas.addRandomShapes() catch {};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dvui.label(@src(), "No document", .{}, .{});
|
dvui.label(@src(), "No document", .{}, .{});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user