Определение позиции дочернего объекта

This commit is contained in:
2026-02-26 21:33:09 +03:00
parent 77604e7b2b
commit 2ab6bcd408
8 changed files with 84 additions and 50 deletions

View File

@@ -13,19 +13,6 @@ const Object = Document.Object;
const DrawContext = pipeline.DrawContext; const DrawContext = pipeline.DrawContext;
const Transform = pipeline.Transform; 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 { 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;
} }
@@ -37,7 +24,7 @@ fn drawObject(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
) !void { ) !void {
if (!isVisible(obj)) return; if (!isVisible(obj)) return;
const local = getLocalTransform(obj); const local = Transform.init(obj);
const world = Transform.compose(parent_transform, local); const world = Transform.compose(parent_transform, local);
ctx.setTransform(world); ctx.setTransform(world);

View File

@@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const dvui = @import("dvui"); const dvui = @import("dvui");
const basic_models = @import("../../models/basic_models.zig"); const basic_models = @import("../../models/basic_models.zig");
const Document = @import("../../models/Document.zig");
const Point2_f = basic_models.Point2_f; const Point2_f = basic_models.Point2_f;
const Point2_i = basic_models.Point2_i; const Point2_i = basic_models.Point2_i;
const Scale2_f = basic_models.Scale2_f; const Scale2_f = basic_models.Scale2_f;
@@ -14,6 +15,19 @@ pub const Transform = struct {
scale: Scale2_f = .{}, scale: Scale2_f = .{},
opacity: f32 = 1.0, 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. /// Композиция: 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);
@@ -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 { pub const DrawContext = struct {
pixels: []Color.PMA, pixels: []Color.PMA,
@@ -100,17 +128,7 @@ pub const DrawContext = struct {
/// Мировые -> локальные. /// Мировые -> локальные.
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; return worldToLocalTransform(self.transform, wx, wy);
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,
};
} }
/// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель. /// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель.

View File

@@ -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 Canvas = @import("../Canvas.zig");
const Document = @import("../models/Document.zig"); const Document = @import("../models/Document.zig");
const pipeline = @import("../render/cpu/pipeline.zig");
const Transform = pipeline.Transform;
pub const ToolContext = struct { pub const ToolContext = struct {
canvas: *Canvas, canvas: *Canvas,
@@ -8,11 +11,41 @@ pub const ToolContext = struct {
selected_object_id: ?u64, selected_object_id: ?u64,
pub fn addObject(self: *const ToolContext, template: Document.Object) !void { 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(); 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 { pub const Tool = struct {
onCanvasClick: *const fn (*const ToolContext) anyerror!void, onCanvasClick: *const fn (*const ToolContext) anyerror!void,
}; };

View File

@@ -5,7 +5,6 @@ fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
const canvas = ctx.canvas; const canvas = ctx.canvas;
var obj = shape.createObject(canvas.document.allocator, .arc) catch return; var obj = shape.createObject(canvas.document.allocator, .arc) catch return;
defer obj.deinit(canvas.allocator); defer obj.deinit(canvas.allocator);
try obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = ctx.document_point } });
try ctx.addObject(obj); try ctx.addObject(obj);
} }
pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick }; pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick };

View File

@@ -5,7 +5,6 @@ fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
const canvas = ctx.canvas; const canvas = ctx.canvas;
var obj = shape.createObject(canvas.document.allocator, .broken) catch return; var obj = shape.createObject(canvas.document.allocator, .broken) catch return;
defer obj.deinit(canvas.allocator); defer obj.deinit(canvas.allocator);
try obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = ctx.document_point } });
try ctx.addObject(obj); try ctx.addObject(obj);
} }
pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick }; pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick };

View File

@@ -5,7 +5,6 @@ fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
const canvas = ctx.canvas; const canvas = ctx.canvas;
var obj = shape.createObject(canvas.document.allocator, .ellipse) catch return; var obj = shape.createObject(canvas.document.allocator, .ellipse) catch return;
defer obj.deinit(canvas.allocator); defer obj.deinit(canvas.allocator);
try obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = ctx.document_point } });
try ctx.addObject(obj); try ctx.addObject(obj);
} }
pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick }; pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick };

View File

@@ -7,7 +7,6 @@ fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
const canvas = 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;
defer obj.deinit(canvas.allocator); defer obj.deinit(canvas.allocator);
try obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = ctx.document_point } });
try ctx.addObject(obj); try ctx.addObject(obj);
} }
pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick }; pub const tool = Tool.Tool{ .onCanvasClick = onCanvasClick };

View File

@@ -58,23 +58,23 @@ pub fn canvasView(canvas: *Canvas, selected_object_id: ?u64, content_rect_scale:
// Панель свойств поверх scroll (правый верхний угол) // Панель свойств поверх scroll (правый верхний угол)
if (selected_object_id) |obj_id| { if (selected_object_id) |obj_id| {
if (canvas.document.findObjectById(obj_id)) |obj| { if (canvas.document.findObjectById(obj_id)) |obj| {
var properties_box = dvui.box( var properties_box = dvui.box(
@src(), @src(),
.{ .dir = .horizontal }, .{ .dir = .horizontal },
.{ .{
.expand = .none, .expand = .none,
.background = false, .background = false,
.gravity_x = 1.0, .gravity_x = 1.0,
.gravity_y = 0.0, .gravity_y = 0.0,
.margin = dvui.Rect{ .w = 32, .y = 16, .h = 100 }, .margin = dvui.Rect{ .w = 32, .y = 16, .h = 100 },
}, },
); );
{ {
drawPropertiesPanel(canvas, obj); drawPropertiesPanel(canvas, obj);
} }
// Сохраняем rect панели свойств для следующего кадра — в handleCanvasMouse исключаем из него клики // Сохраняем rect панели свойств для следующего кадра — в handleCanvasMouse исключаем из него клики
canvas.properties_rect_scale = properties_box.data().contentRectScale(); canvas.properties_rect_scale = properties_box.data().contentRectScale();
properties_box.deinit(); properties_box.deinit();
} }
} }
@@ -192,8 +192,8 @@ fn handleCanvasZoom(canvas: *Canvas, scroll: anytype) void {
}; };
const doc_pt = canvas.contentPointToDocument(content_pt, natural_scale); const doc_pt = canvas.contentPointToDocument(content_pt, natural_scale);
// canvas.addZoom(y / 1000); canvas.addZoom(y / 1000);
canvas.multZoom(1 + y / 3000); // canvas.multZoom(1 + y / 3000);
canvas.requestRedraw(); canvas.requestRedraw();
// Сдвигаем viewport так, чтобы точка под курсором (даже вне холста) не уезжала // Сдвигаем viewport так, чтобы точка под курсором (даже вне холста) не уезжала