Files
Zivro/src/Canvas.zig
2026-03-02 22:43:09 +03:00

243 lines
8.6 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const std = @import("std");
const builtin = @import("builtin");
const dvui = @import("dvui");
const Document = @import("models/Document.zig");
const RenderEngine = @import("render/RenderEngine.zig").RenderEngine;
const basic_models = @import("models/basic_models.zig");
const Rect_i = basic_models.Rect_i;
const Size_i = basic_models.Size_i;
const Point2_f = @import("models/basic_models.zig").Point2_f;
const Color = dvui.Color;
const tools = @import("toolbar/tools.zig");
const Toolbar = @import("toolbar/Toolbar.zig");
const random_document = @import("random_document.zig");
const Canvas = @This();
allocator: std.mem.Allocator,
document: *Document,
render_engine: RenderEngine,
toolbar: Toolbar,
texture: ?dvui.Texture = null,
pos: dvui.Point = dvui.Point{ .x = 400, .y = 400 },
scroll: dvui.ScrollInfo = .{
.vertical = .auto,
.horizontal = .auto,
},
native_scaling: bool = true,
cursor_document_point: ?Point2_f = null,
draw_document: bool = true,
show_render_stats: bool = true,
/// Rect тулбара (из предыдущего кадра) для исключения кликов по нему из handleCanvasMouse.
toolbar_rect_scale: ?dvui.RectScale = null,
/// Rect панели свойств (из предыдущего кадра) для исключения кликов по нему из handleCanvasMouse.
properties_rect_scale: ?dvui.RectScale = null,
redraw_throttle_ms: u32 = 50,
frame_index: u64 = 0,
_zoom: f32 = 1,
_rendering_quality: f32 = 100.0,
_last_redraw_time_ms: i64 = 0, // Метка последней перерисовки чтобы ограничить частоту
_visible_rect: ?Rect_i = null,
_redraw_pending: bool = false,
pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas {
return .{
.allocator = allocator,
.document = document,
.render_engine = engine,
.toolbar = Toolbar.init(&tools.default_tools),
};
}
pub fn deinit(self: *Canvas) void {
self.toolbar.deinit();
if (self.texture) |texture| {
dvui.Texture.destroyLater(texture);
self.texture = null;
}
}
fn redraw(self: *Canvas) !void {
const full = self.getZoomedImageSize();
const vis_full: Rect_i = self._visible_rect orelse Rect_i{ .x = 0, .y = 0, .w = 0, .h = 0 };
if (vis_full.w == 0 or vis_full.h == 0) {
if (self.texture) |tex| {
dvui.Texture.destroyLater(tex);
self.texture = null;
}
return;
}
// Качество рендеринга задаётся в процентах площади (1100),
// при этом фактически уменьшаем ширину/высоту холста на корень из этой доли.
const quality_percent: f32 = self.getRenderingQuality();
const quality_area: f32 = quality_percent / 100.0;
const quality_side: f32 = std.math.sqrt(quality_area);
const scale: f32 = std.math.clamp(quality_side, 0.01, 1.0);
const canvas_size: Size_i = .{
.w = @max(@as(u32, 1), @as(u32, @intFromFloat(@as(f32, @floatFromInt(full.w)) * scale))),
.h = @max(@as(u32, 1), @as(u32, @intFromFloat(@as(f32, @floatFromInt(full.h)) * scale))),
};
var vis_scaled = Rect_i{
.x = @as(u32, @intFromFloat(@as(f32, @floatFromInt(vis_full.x)) * scale)),
.y = @as(u32, @intFromFloat(@as(f32, @floatFromInt(vis_full.y)) * scale)),
.w = @max(@as(u32, 1), @as(u32, @intFromFloat(@as(f32, @floatFromInt(vis_full.w)) * scale))),
.h = @max(@as(u32, 1), @as(u32, @intFromFloat(@as(f32, @floatFromInt(vis_full.h)) * scale))),
};
if (vis_scaled.x >= canvas_size.w or vis_scaled.y >= canvas_size.h) {
if (self.texture) |tex| {
dvui.Texture.destroyLater(tex);
self.texture = null;
}
return;
}
const max_vis_w: u32 = canvas_size.w - vis_scaled.x;
const max_vis_h: u32 = canvas_size.h - vis_scaled.y;
if (vis_scaled.w > max_vis_w) vis_scaled.w = max_vis_w;
if (vis_scaled.h > max_vis_h) vis_scaled.h = max_vis_h;
const new_texture = if (self.draw_document)
self.render_engine.render(self.document, canvas_size, vis_scaled) catch null
else
self.render_engine.example(canvas_size, vis_scaled) catch null;
if (new_texture) |tex| {
if (self.texture) |old_tex| {
dvui.Texture.destroyLater(old_tex);
}
self.texture = tex;
}
self._last_redraw_time_ms = std.time.milliTimestamp();
self.frame_index += 1;
}
pub fn exampleReset(self: *Canvas) !void {
self.render_engine.exampleReset();
try self.redraw();
}
pub fn addRandomShapes(self: *Canvas) !void {
try random_document.addRandomShapes(self.document, self.allocator, std.crypto.random);
self.requestRedraw();
}
pub fn setZoom(self: *Canvas, value: f32) void {
self._zoom = @max(value, 0.01);
}
pub fn addZoom(self: *Canvas, value: f32) void {
self._zoom += value;
self._zoom = @max(self._zoom, 0.01);
}
pub fn multZoom(self: *Canvas, value: f32) void {
self._zoom *= value;
self._zoom = @max(self._zoom, 0.01);
}
pub fn getZoom(self: Canvas) f32 {
return self._zoom;
}
pub fn setRenderingQuality(self: *Canvas, value: f32) void {
self._rendering_quality = std.math.clamp(value, 1.0, 100.0);
self.requestRedraw();
}
pub fn getRenderingQuality(self: Canvas) f32 {
return self._rendering_quality;
}
pub fn requestRedraw(self: *Canvas) void {
self._redraw_pending = true;
}
pub fn processPendingRedraw(self: *Canvas) !void {
if (!self._redraw_pending) return;
if (self.redraw_throttle_ms == 0) {
self._redraw_pending = false;
try self.redraw();
return;
}
const now_ms = std.time.milliTimestamp();
const elapsed: i64 = if (self._last_redraw_time_ms == 0) self.redraw_throttle_ms else now_ms - self._last_redraw_time_ms;
if (elapsed < @as(i64, @intCast(self.redraw_throttle_ms))) return;
self._redraw_pending = false;
try self.redraw();
}
pub fn getZoomedImageSize(self: Canvas) Rect_i {
const doc = self.document;
return .{
.x = @intFromFloat(self.pos.x),
.y = @intFromFloat(self.pos.y),
.w = @intFromFloat(doc.size.w * self._zoom),
.h = @intFromFloat(doc.size.h * self._zoom),
};
}
/// Точка контента -> координаты документа.
pub fn contentPointToDocument(self: Canvas, content_point: dvui.Point, natural_scale: f32) Point2_f {
const img = self.getZoomedImageSize();
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 .{
.x = px_x / self._zoom,
.y = px_y / self._zoom,
};
}
/// Точка контента внутри холста.
pub fn isContentPointOnDocument(self: Canvas, content_point: dvui.Point, natural_scale: f32) bool {
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;
return content_point.x >= left_n and content_point.x < right_n and
content_point.y >= top_n and content_point.y < bottom_n;
}
pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) bool {
const next = computeVisibleImageRect(self.*, viewport, scroll_offset);
var changed = false;
if (self._visible_rect) |vis| {
changed |= next.x != vis.x or next.y != vis.y or next.w != vis.w or next.h != vis.h;
}
self._visible_rect = next;
if (changed or self.texture == null) {
return true;
}
return false;
}
fn computeVisibleImageRect(self: Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) Rect_i {
const image_rect = self.getZoomedImageSize();
const img_w: u32 = image_rect.w;
const img_h: u32 = image_rect.h;
const vis_w: u32 = @min(@as(u32, @intFromFloat(viewport.w)), img_w);
const vis_h: u32 = @min(@as(u32, @intFromFloat(viewport.h)), img_h);
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)));
const vis_x: u32 = @intCast(std.math.clamp(raw_x, 0, @as(i64, img_w) - @as(i64, vis_w)));
const vis_y: u32 = @intCast(std.math.clamp(raw_y, 0, @as(i64, img_h) - @as(i64, vis_h)));
return Rect_i{
.x = vis_x,
.y = vis_y,
.w = vis_w,
.h = vis_h,
};
}