From 2ab6bcd40832062ba7aada3d0cc64a5b1ffe3d0e Mon Sep 17 00:00:00 2001 From: Roman Pytkov Date: Thu, 26 Feb 2026 21:33:09 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=BF=D1=80=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=B7=D0=B8=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BE=D1=87=D0=B5=D1=80=D0=BD=D0=B5=D0=B3=D0=BE?= =?UTF-8?q?=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/render/cpu/draw.zig | 15 +------------ src/render/cpu/pipeline.zig | 40 +++++++++++++++++++++++++---------- src/toolbar/Tool.zig | 37 ++++++++++++++++++++++++++++++-- src/toolbar/tools/arc.zig | 1 - src/toolbar/tools/broken.zig | 1 - src/toolbar/tools/ellipse.zig | 1 - src/toolbar/tools/line.zig | 1 - src/ui/canvas_view.zig | 38 ++++++++++++++++----------------- 8 files changed, 84 insertions(+), 50 deletions(-) diff --git a/src/render/cpu/draw.zig b/src/render/cpu/draw.zig index 21389a3..88750b0 100644 --- a/src/render/cpu/draw.zig +++ b/src/render/cpu/draw.zig @@ -13,19 +13,6 @@ const Object = Document.Object; const DrawContext = pipeline.DrawContext; const Transform = pipeline.Transform; -fn getLocalTransform(obj: *const Object) Transform { - const pos = if (obj.getProperty(.position)) |p| p.position else basic_models.Point2_f{ .x = 0, .y = 0 }; - const angle = if (obj.getProperty(.angle)) |p| p.angle else 0; - const scale = if (obj.getProperty(.scale)) |p| p.scale else basic_models.Scale2_f{ .scale_x = 1, .scale_y = 1 }; - const opacity = if (obj.getProperty(.opacity)) |p| p.opacity else 1.0; - return .{ - .position = pos, - .angle = angle, - .scale = scale, - .opacity = opacity, - }; -} - fn isVisible(obj: *const Object) bool { return if (obj.getProperty(.visible)) |p| p.visible else true; } @@ -37,7 +24,7 @@ fn drawObject( allocator: std.mem.Allocator, ) !void { if (!isVisible(obj)) return; - const local = getLocalTransform(obj); + const local = Transform.init(obj); const world = Transform.compose(parent_transform, local); ctx.setTransform(world); diff --git a/src/render/cpu/pipeline.zig b/src/render/cpu/pipeline.zig index 5dcb79e..73a933b 100644 --- a/src/render/cpu/pipeline.zig +++ b/src/render/cpu/pipeline.zig @@ -1,6 +1,7 @@ const std = @import("std"); const dvui = @import("dvui"); const basic_models = @import("../../models/basic_models.zig"); +const Document = @import("../../models/Document.zig"); const Point2_f = basic_models.Point2_f; const Point2_i = basic_models.Point2_i; const Scale2_f = basic_models.Scale2_f; @@ -14,6 +15,19 @@ pub const Transform = struct { scale: Scale2_f = .{}, opacity: f32 = 1.0, + pub fn init(obj: *const Document.Object) Transform { + const pos = if (obj.getProperty(.position)) |p| p.position else Point2_f{ .x = 0, .y = 0 }; + const angle = if (obj.getProperty(.angle)) |p| p.angle else 0; + const scale = if (obj.getProperty(.scale)) |p| p.scale else Scale2_f{ .scale_x = 1, .scale_y = 1 }; + const opacity = if (obj.getProperty(.opacity)) |p| p.opacity else 1.0; + return .{ + .position = pos, + .angle = angle, + .scale = scale, + .opacity = opacity, + }; + } + /// Композиция: world = parent * local. pub fn compose(parent: Transform, local: Transform) Transform { const cos_a = std.math.cos(parent.angle); @@ -36,6 +50,20 @@ pub const Transform = struct { } }; +/// Мировые -> локальные для заданного трансформа. +pub fn worldToLocalTransform(t: Transform, wx: f32, wy: f32) Point2_f { + const dx = wx - t.position.x; + const dy = wy - t.position.y; + const cos_a = std.math.cos(-t.angle); + const sin_a = std.math.sin(-t.angle); + const sx = if (t.scale.scale_x != 0) t.scale.scale_x else 1.0; + const sy = if (t.scale.scale_y != 0) t.scale.scale_y else 1.0; + return .{ + .x = (dx * cos_a - dy * sin_a) / sx, + .y = (dx * sin_a + dy * cos_a) / sy, + }; +} + /// Конвейер отрисовки: локальные координаты -> трансформ -> буфер. pub const DrawContext = struct { pixels: []Color.PMA, @@ -100,17 +128,7 @@ pub const DrawContext = struct { /// Мировые -> локальные. pub fn worldToLocal(self: *const DrawContext, wx: f32, wy: f32) Point2_f { - const t = &self.transform; - const dx = wx - t.position.x; - const dy = wy - t.position.y; - const cos_a = std.math.cos(-t.angle); - const sin_a = std.math.sin(-t.angle); - const sx = if (t.scale.scale_x != 0) t.scale.scale_x else 1.0; - const sy = if (t.scale.scale_y != 0) t.scale.scale_y else 1.0; - return .{ - .x = (dx * cos_a - dy * sin_a) / sx, - .y = (dx * sin_a + dy * cos_a) / sy, - }; + return worldToLocalTransform(self.transform, wx, wy); } /// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель. diff --git a/src/toolbar/Tool.zig b/src/toolbar/Tool.zig index f1857c8..c2b75bb 100644 --- a/src/toolbar/Tool.zig +++ b/src/toolbar/Tool.zig @@ -1,6 +1,9 @@ -const Point2_f = @import("../models/basic_models.zig").Point2_f; +const basic_models = @import("../models/basic_models.zig"); +const Point2_f = basic_models.Point2_f; const Canvas = @import("../Canvas.zig"); const Document = @import("../models/Document.zig"); +const pipeline = @import("../render/cpu/pipeline.zig"); +const Transform = pipeline.Transform; pub const ToolContext = struct { canvas: *Canvas, @@ -8,11 +11,41 @@ pub const ToolContext = struct { selected_object_id: ?u64, pub fn addObject(self: *const ToolContext, template: Document.Object) !void { - try self.canvas.document.addObjectUnderParentId(self.selected_object_id, template); + var obj = template; + const local_pos = self.computeLocalPosition(); + try obj.setProperty(self.canvas.document.allocator, .{ .data = .{ .position = local_pos } }); + try self.canvas.document.addObjectUnderParentId(self.selected_object_id, obj); self.canvas.requestRedraw(); } + + fn computeLocalPosition(self: *const ToolContext) Point2_f { + if (self.selected_object_id) |parent_id| { + if (findWorldTransformById(self.canvas.document, parent_id)) |parent_world| { + return pipeline.worldToLocalTransform(parent_world, self.document_point.x, self.document_point.y); + } + } + return self.document_point; + } }; +fn findWorldTransformById(doc: *Document, target_id: u64) ?Transform { + const identity = Transform{}; + for (doc.objects.items) |*obj| { + if (findWorldTransformInTree(obj, identity, target_id)) |t| return t; + } + return null; +} + +fn findWorldTransformInTree(obj: *const Document.Object, parent: Transform, target_id: u64) ?Transform { + const local = Transform.init(obj); + const world = Transform.compose(parent, local); + if (obj.id == target_id) return world; + for (obj.children.items) |*child| { + if (findWorldTransformInTree(child, world, target_id)) |t| return t; + } + return null; +} + pub const Tool = struct { onCanvasClick: *const fn (*const ToolContext) anyerror!void, }; diff --git a/src/toolbar/tools/arc.zig b/src/toolbar/tools/arc.zig index c91fe19..93d2720 100644 --- a/src/toolbar/tools/arc.zig +++ b/src/toolbar/tools/arc.zig @@ -5,7 +5,6 @@ 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 ctx.addObject(obj); } pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick }; diff --git a/src/toolbar/tools/broken.zig b/src/toolbar/tools/broken.zig index 30617a1..6a491c3 100644 --- a/src/toolbar/tools/broken.zig +++ b/src/toolbar/tools/broken.zig @@ -5,7 +5,6 @@ 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 ctx.addObject(obj); } pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick }; diff --git a/src/toolbar/tools/ellipse.zig b/src/toolbar/tools/ellipse.zig index 93222a2..9364633 100644 --- a/src/toolbar/tools/ellipse.zig +++ b/src/toolbar/tools/ellipse.zig @@ -5,7 +5,6 @@ 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 ctx.addObject(obj); } pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick }; diff --git a/src/toolbar/tools/line.zig b/src/toolbar/tools/line.zig index 84fd8c3..d833db0 100644 --- a/src/toolbar/tools/line.zig +++ b/src/toolbar/tools/line.zig @@ -7,7 +7,6 @@ fn onCanvasClick(ctx: *const Tool.ToolContext) !void { const canvas = ctx.canvas; var obj = shape.createObject(canvas.document.allocator, .line) catch return; defer obj.deinit(canvas.allocator); - try obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = ctx.document_point } }); try ctx.addObject(obj); } pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick }; diff --git a/src/ui/canvas_view.zig b/src/ui/canvas_view.zig index 9e4a6b3..8c15f25 100644 --- a/src/ui/canvas_view.zig +++ b/src/ui/canvas_view.zig @@ -58,23 +58,23 @@ pub fn canvasView(canvas: *Canvas, selected_object_id: ?u64, content_rect_scale: // Панель свойств поверх scroll (правый верхний угол) if (selected_object_id) |obj_id| { if (canvas.document.findObjectById(obj_id)) |obj| { - var properties_box = dvui.box( - @src(), - .{ .dir = .horizontal }, - .{ - .expand = .none, - .background = false, - .gravity_x = 1.0, - .gravity_y = 0.0, - .margin = dvui.Rect{ .w = 32, .y = 16, .h = 100 }, - }, - ); - { - drawPropertiesPanel(canvas, obj); - } - // Сохраняем rect панели свойств для следующего кадра — в handleCanvasMouse исключаем из него клики - canvas.properties_rect_scale = properties_box.data().contentRectScale(); - properties_box.deinit(); + var properties_box = dvui.box( + @src(), + .{ .dir = .horizontal }, + .{ + .expand = .none, + .background = false, + .gravity_x = 1.0, + .gravity_y = 0.0, + .margin = dvui.Rect{ .w = 32, .y = 16, .h = 100 }, + }, + ); + { + drawPropertiesPanel(canvas, obj); + } + // Сохраняем rect панели свойств для следующего кадра — в handleCanvasMouse исключаем из него клики + canvas.properties_rect_scale = properties_box.data().contentRectScale(); + properties_box.deinit(); } } @@ -192,8 +192,8 @@ fn handleCanvasZoom(canvas: *Canvas, scroll: anytype) void { }; const doc_pt = canvas.contentPointToDocument(content_pt, natural_scale); - // canvas.addZoom(y / 1000); - canvas.multZoom(1 + y / 3000); + canvas.addZoom(y / 1000); + // canvas.multZoom(1 + y / 3000); canvas.requestRedraw(); // Сдвигаем viewport так, чтобы точка под курсором (даже вне холста) не уезжала