Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codecov.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ coverage:

patch:
default:
target: 90%
target: 80%
threshold: 0%
base: auto
if_no_uploads: error
Expand Down
13 changes: 8 additions & 5 deletions components/Canvas.qml
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,6 @@ Item {
onOverlayRotatingChanged: function (isRotating) {
if (isRotating) {
canvasModel.beginTransaction();
// Move origin to center BEFORE rotation starts to prevent visual jump
var idx = Lucent.SelectionManager.selectedItemIndex;
if (idx >= 0) {
canvasModel.ensureOriginCentered(idx);
}
} else {
canvasModel.endTransaction();
}
Expand Down Expand Up @@ -124,6 +119,14 @@ Item {
pathEditController.deleteSelectedPoints();
}

function lockPathEditTransform() {
pathEditController.lockTransformForDrag();
}

function unlockPathEditTransform() {
pathEditController.unlockTransformAfterDrag();
}

// Tiled rendering layer
TiledShapesLayer {
id: shapesLayer
Expand Down
37 changes: 20 additions & 17 deletions components/PathEditController.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,9 @@ QtObject {
readonly property int selectedItemIndex: Lucent.SelectionManager.selectedItemIndex
readonly property var selectedItem: Lucent.SelectionManager.selectedItem

// Pre-transformed points from Python (screen space)
// Depends on selectedItem to re-evaluate when geometry changes
readonly property var transformedPoints: {
if (!editModeActive || selectedItemIndex < 0)
return null;
// Force re-evaluation when selectedItem changes
var item = selectedItem;
if (!item)
return null;
Expand All @@ -35,6 +32,18 @@ QtObject {

readonly property var selectedPointIndices: Lucent.SelectionManager.selectedPointIndices

function lockTransformForDrag() {
if (selectedItemIndex >= 0) {
canvasModel.lockEditTransform(selectedItemIndex);
}
}

function unlockTransformAfterDrag() {
if (selectedItemIndex >= 0) {
canvasModel.unlockEditTransform(selectedItemIndex);
}
}

function handlePointClicked(index, modifiers) {
var multi = modifiers & Qt.ShiftModifier;
Lucent.SelectionManager.selectPoint(index, multi);
Expand All @@ -49,8 +58,7 @@ QtObject {
if (!item || item.type !== "path")
return;

// Convert screen position to geometry space using Python
var geomPos = canvasModel.transformPointToGeometry(idx, screenX, screenY);
var geomPos = canvasModel.transformPointToGeometryLocked(idx, screenX, screenY);
if (!geomPos)
return;

Expand Down Expand Up @@ -87,11 +95,9 @@ QtObject {
}
}

canvasModel.updateItem(idx, {
geometry: {
points: newPoints,
closed: item.geometry.closed
}
canvasModel.updateGeometryWithOriginCompensation(idx, {
points: newPoints,
closed: item.geometry.closed
});
}

Expand All @@ -104,8 +110,7 @@ QtObject {
if (!item || item.type !== "path")
return;

// Convert screen position to geometry space using Python
var geomPos = canvasModel.transformPointToGeometry(idx, screenX, screenY);
var geomPos = canvasModel.transformPointToGeometryLocked(idx, screenX, screenY);
if (!geomPos)
return;

Expand Down Expand Up @@ -170,11 +175,9 @@ QtObject {
}
}

canvasModel.updateItem(idx, {
geometry: {
points: newPoints,
closed: item.geometry.closed
}
canvasModel.updateGeometryWithOriginCompensation(idx, {
points: newPoints,
closed: item.geometry.closed
});
}

Expand Down
1 change: 0 additions & 1 deletion components/PathEditOverlay.qml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ Item {

readonly property var points: transformedPoints || []
readonly property bool isClosed: pathGeometry ? pathGeometry.closed : false

readonly property real handleSize: 10 / zoomLevel
readonly property real handleLineWidth: 1 / zoomLevel

Expand Down
4 changes: 4 additions & 0 deletions components/Viewport.qml
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,13 @@ Item {

onDragStarted: {
canvasModel.beginTransaction();
if (overlayContainer.canvasRef)
overlayContainer.canvasRef.lockPathEditTransform();
}

onDragEnded: {
if (overlayContainer.canvasRef)
overlayContainer.canvasRef.unlockPathEditTransform();
canvasModel.endTransaction();
}
}
Expand Down
27 changes: 25 additions & 2 deletions components/panels/TransformPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,29 @@ Item {
readonly property real currentRotation: currentTransform ? (currentTransform.rotate ?? 0) : 0
readonly property real currentOriginX: currentTransform ? (currentTransform.originX ?? 0) : 0
readonly property real currentOriginY: currentTransform ? (currentTransform.originY ?? 0) : 0
readonly property real originSnapTolerance: 0.02
readonly property bool hasPresetOrigin: _isPresetOrigin(currentOriginX, currentOriginY)

function _snapOrigin(value) {
var targets = [0, 0.5, 1];
for (var i = 0; i < targets.length; i++) {
if (Math.abs(value - targets[i]) <= originSnapTolerance)
return targets[i];
}
return value;
}

function _isPresetOrigin(x, y) {
var targets = [0, 0.5, 1];
for (var row = 0; row < targets.length; row++) {
for (var col = 0; col < targets.length; col++) {
if (Math.abs(x - targets[col]) <= originSnapTolerance && Math.abs(y - targets[row]) <= originSnapTolerance) {
return true;
}
}
}
return false;
}

implicitHeight: contentLayout.implicitHeight

Expand Down Expand Up @@ -175,14 +198,14 @@ Item {
width: 16
height: 16
checkable: true
checked: root.currentOriginX === modelData.ox && root.currentOriginY === modelData.oy
checked: root._snapOrigin(root.currentOriginX) === modelData.ox && root._snapOrigin(root.currentOriginY) === modelData.oy
ButtonGroup.group: originGroup

onClicked: canvasModel.setItemOrigin(root.selectedIndex, modelData.ox, modelData.oy)

background: Rectangle {
color: parent.checked ? root.themePalette.highlight : root.themePalette.button
border.color: root.themePalette.mid
border.color: root.hasPresetOrigin ? root.themePalette.mid : "#ffffff"
border.width: 1
radius: 2
}
Expand Down
4 changes: 2 additions & 2 deletions components/tools/PenTool.qml
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ Item {

var originX = width / 2;
var originY = height / 2;
var handleLineColor = Lucent.Themed.palette.highlight.toString();
var handleGripColor = Lucent.Themed.palette.highlight.toString();
var handleLineColor = Lucent.Themed.selector.toString();
var handleGripColor = Lucent.Themed.selector.toString();

ctx.save();
ctx.lineWidth = 1;
Expand Down
62 changes: 42 additions & 20 deletions src/lucent/canvas_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,13 @@ def __init__(
) -> None:
self.geometry = geometry
self.appearances = appearances
self.transform = transform or Transform()
if transform is None:
bounds = geometry.get_bounds()
transform = Transform(
pivot_x=bounds.x() + bounds.width() * 0.5,
pivot_y=bounds.y() + bounds.height() * 0.5,
)
self.transform = transform
self.name = name
self.parent_id = parent_id
self.visible = bool(visible)
Expand Down Expand Up @@ -120,10 +126,9 @@ def paint(

path = self.geometry.to_painter_path()
if not self.transform.is_identity():
bounds = self.geometry.get_bounds()
origin_x = bounds.x() + bounds.width() * self.transform.origin_x
origin_y = bounds.y() + bounds.height() * self.transform.origin_y
qtransform = self.transform.to_qtransform_centered(origin_x, origin_y)
qtransform = self.transform.to_qtransform_centered(
self.transform.pivot_x, self.transform.pivot_y
)
path = qtransform.map(path)

# Check if stroke should render below fill
Expand All @@ -146,9 +151,9 @@ def get_bounds(self) -> QRectF:
"""Return bounding rectangle in canvas coordinates."""
bounds = self.geometry.get_bounds()
if not self.transform.is_identity():
origin_x = bounds.x() + bounds.width() * self.transform.origin_x
origin_y = bounds.y() + bounds.height() * self.transform.origin_y
qtransform = self.transform.to_qtransform_centered(origin_x, origin_y)
qtransform = self.transform.to_qtransform_centered(
self.transform.pivot_x, self.transform.pivot_y
)
return qtransform.mapRect(bounds)
return bounds

Expand All @@ -158,6 +163,13 @@ def from_dict(data: Dict[str, Any]) -> "ShapeItem":
raise NotImplementedError("ShapeItem.from_dict must be overridden")


def _transform_from_data(
transform_data: Dict[str, Any], geometry: Geometry
) -> Transform:
"""Create Transform from data."""
return Transform.from_dict(transform_data)


class RectangleItem(ShapeItem):
"""Rectangle canvas item."""

Expand Down Expand Up @@ -193,7 +205,9 @@ def from_dict(data: Dict[str, Any]) -> "RectangleItem":
)
appearances = [Appearance.from_dict(a) for a in data.get("appearances", [])]
transform = (
Transform.from_dict(data["transform"]) if "transform" in data else None
_transform_from_data(data["transform"], geometry)
if "transform" in data
else None
)
return RectangleItem(
geometry=geometry,
Expand Down Expand Up @@ -241,7 +255,9 @@ def from_dict(data: Dict[str, Any]) -> "EllipseItem":
)
appearances = [Appearance.from_dict(a) for a in data.get("appearances", [])]
transform = (
Transform.from_dict(data["transform"]) if "transform" in data else None
_transform_from_data(data["transform"], geometry)
if "transform" in data
else None
)
return EllipseItem(
geometry=geometry,
Expand Down Expand Up @@ -287,7 +303,9 @@ def from_dict(data: Dict[str, Any]) -> "PathItem":
)
appearances = [Appearance.from_dict(a) for a in data.get("appearances", [])]
transform = (
Transform.from_dict(data["transform"]) if "transform" in data else None
_transform_from_data(data["transform"], geometry)
if "transform" in data
else None
)
return PathItem(
geometry=geometry,
Expand Down Expand Up @@ -473,11 +491,9 @@ def paint(

# Apply transform if not identity
if not self.transform.is_identity():
bounds = geom.get_bounds()
# Calculate origin point based on transform origin settings
origin_x = bounds.x() + bounds.width() * self.transform.origin_x
origin_y = bounds.y() + bounds.height() * self.transform.origin_y
qtransform = self.transform.to_qtransform_centered(origin_x, origin_y)
qtransform = self.transform.to_qtransform_centered(
self.transform.pivot_x, self.transform.pivot_y
)

# Apply offset, then transform, then translate to geometry position
painter.translate(offset_x, offset_y)
Expand Down Expand Up @@ -508,9 +524,13 @@ def get_bounds(self) -> QRectF:

# Apply transform if not identity
if not self.transform.is_identity():
origin_x = bounds.x() + bounds.width() * self.transform.origin_x
origin_y = bounds.y() + bounds.height() * self.transform.origin_y
qtransform = self.transform.to_qtransform_centered(origin_x, origin_y)
qtransform = self.transform.to_qtransform_centered(
self.transform.pivot_x, self.transform.pivot_y
)
if not self.transform.is_identity():
qtransform = self.transform.to_qtransform_centered(
self.transform.pivot_x, self.transform.pivot_y
)
return qtransform.mapRect(bounds)
return bounds

Expand All @@ -525,7 +545,9 @@ def from_dict(data: Dict[str, Any]) -> "TextItem":
height=float(geom.get("height", data.get("height", 0))),
)
transform = (
Transform.from_dict(data["transform"]) if "transform" in data else None
_transform_from_data(data["transform"], geometry)
if "transform" in data
else None
)
return TextItem(
geometry=geometry,
Expand Down
Loading