2026-05-19 10:21:13 -05:00
|
|
|
import math
|
|
|
|
|
|
2026-05-19 11:02:10 -05:00
|
|
|
from krita import DockWidget, DockWidgetFactory, DockWidgetFactoryBase, Krita
|
2026-05-19 10:21:13 -05:00
|
|
|
from PyQt5.QtCore import QPointF
|
|
|
|
|
from PyQt5.QtWidgets import (
|
|
|
|
|
QCheckBox,
|
|
|
|
|
QComboBox,
|
|
|
|
|
QDoubleSpinBox,
|
|
|
|
|
QHBoxLayout,
|
|
|
|
|
QLabel,
|
|
|
|
|
QPushButton,
|
|
|
|
|
QVBoxLayout,
|
2026-05-19 11:02:10 -05:00
|
|
|
QWidget,
|
2026-05-19 10:21:13 -05:00
|
|
|
)
|
|
|
|
|
|
2026-05-19 11:02:10 -05:00
|
|
|
DOCKER_ID = "grid_generator_docker"
|
2026-05-19 10:21:13 -05:00
|
|
|
|
2026-05-19 11:02:10 -05:00
|
|
|
|
|
|
|
|
class GridDocker(DockWidget):
|
|
|
|
|
def __init__(self):
|
|
|
|
|
super().__init__()
|
2026-05-19 10:21:13 -05:00
|
|
|
self.setWindowTitle("Grid Generator")
|
|
|
|
|
self.setup_ui()
|
|
|
|
|
|
|
|
|
|
def setup_ui(self):
|
2026-05-19 11:02:10 -05:00
|
|
|
main_widget = QWidget()
|
|
|
|
|
layout = QVBoxLayout(main_widget)
|
2026-05-19 10:21:13 -05:00
|
|
|
|
|
|
|
|
# Thickness
|
|
|
|
|
thickness_layout = QHBoxLayout()
|
2026-05-19 11:02:10 -05:00
|
|
|
thickness_layout.addWidget(QLabel("Thickness (px):"))
|
2026-05-19 10:21:13 -05:00
|
|
|
self.thickness_spin = QDoubleSpinBox()
|
|
|
|
|
self.thickness_spin.setRange(0.1, 1000.0)
|
|
|
|
|
self.thickness_spin.setValue(2.0)
|
|
|
|
|
thickness_layout.addWidget(self.thickness_spin)
|
|
|
|
|
layout.addLayout(thickness_layout)
|
|
|
|
|
|
|
|
|
|
# Width
|
2026-05-19 11:09:20 -05:00
|
|
|
self.width_container = QWidget()
|
|
|
|
|
width_layout = QHBoxLayout(self.width_container)
|
|
|
|
|
width_layout.setContentsMargins(0, 0, 0, 0)
|
2026-05-19 11:02:10 -05:00
|
|
|
width_layout.addWidget(QLabel("Width (px):"))
|
2026-05-19 10:21:13 -05:00
|
|
|
self.width_spin = QDoubleSpinBox()
|
|
|
|
|
self.width_spin.setRange(1.0, 10000.0)
|
|
|
|
|
self.width_spin.setValue(100.0)
|
|
|
|
|
self.width_spin.valueChanged.connect(self.on_width_changed)
|
|
|
|
|
width_layout.addWidget(self.width_spin)
|
2026-05-19 11:09:20 -05:00
|
|
|
layout.addWidget(self.width_container)
|
2026-05-19 10:21:13 -05:00
|
|
|
|
|
|
|
|
# Height
|
2026-05-19 11:09:20 -05:00
|
|
|
self.height_container = QWidget()
|
|
|
|
|
height_layout = QHBoxLayout(self.height_container)
|
|
|
|
|
height_layout.setContentsMargins(0, 0, 0, 0)
|
2026-05-19 11:02:10 -05:00
|
|
|
height_layout.addWidget(QLabel("Height (px):"))
|
2026-05-19 10:21:13 -05:00
|
|
|
self.height_spin = QDoubleSpinBox()
|
|
|
|
|
self.height_spin.setRange(1.0, 10000.0)
|
|
|
|
|
self.height_spin.setValue(100.0)
|
|
|
|
|
self.height_spin.valueChanged.connect(self.on_height_changed)
|
|
|
|
|
height_layout.addWidget(self.height_spin)
|
2026-05-19 11:09:20 -05:00
|
|
|
layout.addWidget(self.height_container)
|
|
|
|
|
|
|
|
|
|
# Side Length (Hexagon Only)
|
|
|
|
|
self.side_container = QWidget()
|
|
|
|
|
side_layout = QHBoxLayout(self.side_container)
|
|
|
|
|
side_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
side_layout.addWidget(QLabel("Side Length (px):"))
|
|
|
|
|
self.side_spin = QDoubleSpinBox()
|
|
|
|
|
self.side_spin.setRange(1.0, 10000.0)
|
|
|
|
|
self.side_spin.setValue(50.0)
|
|
|
|
|
side_layout.addWidget(self.side_spin)
|
|
|
|
|
layout.addWidget(self.side_container)
|
|
|
|
|
self.side_container.hide()
|
2026-05-19 10:21:13 -05:00
|
|
|
|
|
|
|
|
# Lock Width/Height
|
|
|
|
|
self.lock_check = QCheckBox("Lock Aspect Ratio")
|
|
|
|
|
self.lock_check.setChecked(True)
|
|
|
|
|
layout.addWidget(self.lock_check)
|
|
|
|
|
|
|
|
|
|
# Grid Type
|
|
|
|
|
type_layout = QHBoxLayout()
|
2026-05-19 11:02:10 -05:00
|
|
|
type_layout.addWidget(QLabel("Type:"))
|
2026-05-19 10:21:13 -05:00
|
|
|
self.type_combo = QComboBox()
|
|
|
|
|
self.type_combo.addItems(["Square", "Hexagon"])
|
2026-05-19 11:09:20 -05:00
|
|
|
self.type_combo.currentTextChanged.connect(self.on_type_changed)
|
2026-05-19 10:21:13 -05:00
|
|
|
type_layout.addWidget(self.type_combo)
|
|
|
|
|
layout.addLayout(type_layout)
|
|
|
|
|
|
2026-05-19 11:02:10 -05:00
|
|
|
# Color Option
|
|
|
|
|
color_layout = QHBoxLayout()
|
|
|
|
|
color_layout.addWidget(QLabel("Color:"))
|
|
|
|
|
self.color_combo = QComboBox()
|
|
|
|
|
self.color_combo.addItems(["Foreground", "Background"])
|
|
|
|
|
color_layout.addWidget(self.color_combo)
|
|
|
|
|
layout.addLayout(color_layout)
|
|
|
|
|
|
2026-05-19 10:21:13 -05:00
|
|
|
# Buttons
|
|
|
|
|
self.ok_button = QPushButton("Generate")
|
|
|
|
|
self.ok_button.clicked.connect(self.generate)
|
2026-05-19 11:02:10 -05:00
|
|
|
layout.addWidget(self.ok_button)
|
|
|
|
|
|
|
|
|
|
layout.addStretch()
|
|
|
|
|
self.setWidget(main_widget)
|
2026-05-19 10:21:13 -05:00
|
|
|
|
2026-05-19 11:09:20 -05:00
|
|
|
def on_type_changed(self, grid_type):
|
|
|
|
|
is_square = grid_type == "Square"
|
|
|
|
|
self.width_container.setVisible(is_square)
|
|
|
|
|
self.height_container.setVisible(is_square)
|
|
|
|
|
self.lock_check.setVisible(is_square)
|
|
|
|
|
self.side_container.setVisible(not is_square)
|
|
|
|
|
|
2026-05-19 10:21:13 -05:00
|
|
|
def on_width_changed(self, value):
|
|
|
|
|
if self.lock_check.isChecked():
|
|
|
|
|
self.height_spin.blockSignals(True)
|
|
|
|
|
self.height_spin.setValue(value)
|
|
|
|
|
self.height_spin.blockSignals(False)
|
|
|
|
|
|
|
|
|
|
def on_height_changed(self, value):
|
|
|
|
|
if self.lock_check.isChecked():
|
|
|
|
|
self.width_spin.blockSignals(True)
|
|
|
|
|
self.width_spin.setValue(value)
|
|
|
|
|
self.width_spin.blockSignals(False)
|
|
|
|
|
|
2026-05-19 11:02:10 -05:00
|
|
|
def canvasChanged(self, canvas):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def get_color_hex(self):
|
|
|
|
|
app = Krita.instance()
|
|
|
|
|
window = app.activeWindow()
|
|
|
|
|
if not window:
|
|
|
|
|
return "#000000"
|
|
|
|
|
|
|
|
|
|
view = window.activeView()
|
|
|
|
|
if not view:
|
|
|
|
|
return "#000000"
|
|
|
|
|
|
|
|
|
|
color = None
|
|
|
|
|
if self.color_combo.currentText() == "Foreground":
|
|
|
|
|
color = view.foregroundColor()
|
|
|
|
|
else:
|
|
|
|
|
color = view.backgroundColor()
|
|
|
|
|
|
|
|
|
|
color.setColorSpace("RGBA", "U8", "sRGB built-in")
|
|
|
|
|
comps = color.componentsOrdered()
|
|
|
|
|
|
|
|
|
|
r = int(max(0, min(1, comps[0])) * 255)
|
|
|
|
|
g = int(max(0, min(1, comps[1])) * 255)
|
|
|
|
|
b = int(max(0, min(1, comps[2])) * 255)
|
|
|
|
|
|
|
|
|
|
return f"#{r:02x}{g:02x}{b:02x}"
|
|
|
|
|
|
2026-05-19 10:21:13 -05:00
|
|
|
def generate(self):
|
|
|
|
|
app = Krita.instance()
|
|
|
|
|
doc = app.activeDocument()
|
|
|
|
|
if not doc:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
thickness = self.thickness_spin.value()
|
|
|
|
|
grid_type = self.type_combo.currentText()
|
2026-05-19 11:02:10 -05:00
|
|
|
color_hex = self.get_color_hex()
|
2026-05-19 10:21:13 -05:00
|
|
|
|
2026-05-19 11:09:20 -05:00
|
|
|
# Always create a new vector layer for the grid
|
|
|
|
|
target_node = doc.createVectorLayer("grid")
|
|
|
|
|
doc.rootNode().addChildNode(target_node, None)
|
2026-05-19 10:21:13 -05:00
|
|
|
|
2026-05-19 11:02:10 -05:00
|
|
|
if hasattr(target_node, "addShapesFromSvg"):
|
2026-05-19 11:09:20 -05:00
|
|
|
if grid_type == "Square":
|
|
|
|
|
width = self.width_spin.value()
|
|
|
|
|
height = self.height_spin.value()
|
|
|
|
|
self.draw_square_grid(
|
|
|
|
|
target_node, doc, thickness, width, height, color_hex
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
side_length = self.side_spin.value()
|
|
|
|
|
self.draw_hexagon_grid(
|
|
|
|
|
target_node, doc, thickness, side_length, color_hex
|
|
|
|
|
)
|
2026-05-19 10:21:13 -05:00
|
|
|
else:
|
2026-05-19 11:02:10 -05:00
|
|
|
print(
|
2026-05-19 11:09:20 -05:00
|
|
|
"Error: Target node does not support SVG shapes. Please ensure you are using Krita 5.0+."
|
2026-05-19 11:02:10 -05:00
|
|
|
)
|
2026-05-19 10:21:13 -05:00
|
|
|
|
2026-05-19 11:02:10 -05:00
|
|
|
doc.refreshProjection()
|
2026-05-19 10:21:13 -05:00
|
|
|
|
2026-05-19 11:02:10 -05:00
|
|
|
def draw_square_grid(self, node, doc, thickness, width, height, color_hex):
|
|
|
|
|
svg = f'<svg><g fill="none" stroke="{color_hex}" stroke-width="{thickness}">'
|
2026-05-19 10:21:13 -05:00
|
|
|
x = 0
|
|
|
|
|
while x <= doc.width():
|
|
|
|
|
svg += f'<line x1="{x}" y1="0" x2="{x}" y2="{doc.height()}" />'
|
|
|
|
|
x += width
|
|
|
|
|
y = 0
|
|
|
|
|
while y <= doc.height():
|
|
|
|
|
svg += f'<line x1="0" y1="{y}" x2="{doc.width()}" y2="{y}" />'
|
|
|
|
|
y += height
|
|
|
|
|
svg += "</g></svg>"
|
|
|
|
|
node.addShapesFromSvg(svg)
|
|
|
|
|
|
2026-05-19 11:09:20 -05:00
|
|
|
def draw_hexagon_grid(self, node, doc, thickness, side_length, color_hex):
|
|
|
|
|
h_width = math.sqrt(3) * side_length
|
|
|
|
|
v_spacing = 1.5 * side_length
|
|
|
|
|
|
2026-05-19 11:02:10 -05:00
|
|
|
svg = f'<svg><g fill="none" stroke="{color_hex}" stroke-width="{thickness}">'
|
2026-05-19 11:09:20 -05:00
|
|
|
|
2026-05-19 10:21:13 -05:00
|
|
|
rows = int(doc.height() / v_spacing) + 2
|
2026-05-19 11:09:20 -05:00
|
|
|
cols = int(doc.width() / h_width) + 2
|
|
|
|
|
|
2026-05-19 10:21:13 -05:00
|
|
|
for r in range(rows):
|
2026-05-19 11:09:20 -05:00
|
|
|
offset_x = (h_width / 2) if (r % 2 == 1) else 0
|
2026-05-19 10:21:13 -05:00
|
|
|
for c in range(cols):
|
2026-05-19 11:09:20 -05:00
|
|
|
cx = c * h_width + offset_x
|
2026-05-19 10:21:13 -05:00
|
|
|
cy = r * v_spacing
|
2026-05-19 11:09:20 -05:00
|
|
|
|
|
|
|
|
v = []
|
|
|
|
|
for i in range(6):
|
|
|
|
|
angle_deg = 60 * i - 90
|
|
|
|
|
angle_rad = math.pi / 180 * angle_deg
|
|
|
|
|
px = cx + side_length * math.cos(angle_rad)
|
|
|
|
|
py = cy + side_length * math.sin(angle_rad)
|
|
|
|
|
v.append((px, py))
|
|
|
|
|
|
2026-05-19 10:21:13 -05:00
|
|
|
points = " ".join([f"{px},{py}" for px, py in v])
|
|
|
|
|
svg += f'<polygon points="{points}" />'
|
2026-05-19 11:09:20 -05:00
|
|
|
|
2026-05-19 10:21:13 -05:00
|
|
|
svg += "</g></svg>"
|
|
|
|
|
node.addShapesFromSvg(svg)
|
2026-05-19 11:02:10 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
Krita.instance().addDockWidgetFactory(
|
|
|
|
|
DockWidgetFactory(DOCKER_ID, DockWidgetFactoryBase.DockRight, GridDocker)
|
|
|
|
|
)
|