mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Migrate to using MoveSelectedLayersTo (#469)
* migrate to using MoveSelectedLayersTo * Fix dragging a selected layer with multiple selected layers * Fix CreatedLayer overriding selection * Fix MoveSelectedLayersTo behaviour * Squashed commit of the following: commit095d577a49Author: Keavon Chambers <keavon@keavon.com> Date: Mon Jan 10 18:06:12 2022 -0800 Fix NumberInput clamping regression with undefined bounds commit9f54a376c4Author: mfish33 <32677537+mfish33@users.noreply.github.com> Date: Sun Jan 9 15:52:55 2022 -0800 Fix bounds with artboards for zoom-to-fit and scrollbar scaling (#473) * - document load keeps postition - zoom to fit - scrollbars use artboard dimensions * - review comments - svg export uses all artboard bounds Co-authored-by: Keavon Chambers <keavon@keavon.com> commit61432de480Author: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Sat Jan 8 21:06:15 2022 +0000 Fix rotation input (#472) * Fix insert with no nesting at end of panel * Deselect other layers on paste * Resolve logging
This commit is contained in:
parent
574028375b
commit
c76c92e500
4 changed files with 83 additions and 104 deletions
|
|
@ -174,11 +174,6 @@ pub enum DocumentMessage {
|
|||
insert_index: isize,
|
||||
},
|
||||
ReorderSelectedLayers(i32), // relative_position,
|
||||
MoveLayerInTree {
|
||||
layer: Vec<LayerId>,
|
||||
insert_above: bool,
|
||||
neighbor: Vec<LayerId>,
|
||||
},
|
||||
SetSnapping(bool),
|
||||
ZoomCanvasToFitAll,
|
||||
}
|
||||
|
|
@ -537,6 +532,16 @@ impl DocumentMessageHandler {
|
|||
Some(layer_panel_entry(layer_metadata, transform, layer, path.to_vec()))
|
||||
}
|
||||
|
||||
/// When working with an insert index, deleting the layers may cause the insert index to point to a different location (if the layer being deleted was located before the insert index).
|
||||
///
|
||||
/// This function updates the insert index so that it points to the same place after the specified `layers` are deleted.
|
||||
fn update_insert_index<'a>(&self, layers: &[&'a [LayerId]], path: &[LayerId], insert_index: isize) -> Result<isize, DocumentError> {
|
||||
let folder = self.graphene_document.folder(path)?;
|
||||
let layer_ids_above = if insert_index < 0 { &folder.layer_ids } else { &folder.layer_ids[..(insert_index as usize)] };
|
||||
|
||||
Ok(insert_index - layer_ids_above.iter().filter(|layer_id| layers.iter().any(|x| *x == [path, &[**layer_id]].concat())).count() as isize)
|
||||
}
|
||||
|
||||
pub fn document_bounds(&self) -> Option<[DVec2; 2]> {
|
||||
if self.artboard_message_handler.is_infinite_canvas() {
|
||||
self.graphene_document.viewport_bounding_box(&[]).ok().flatten()
|
||||
|
|
@ -842,10 +847,14 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
DocumentResponse::LayerChanged { path } => responses.push_back(LayerChanged(path.clone()).into()),
|
||||
DocumentResponse::CreatedLayer { path } => {
|
||||
if self.layer_metadata.contains_key(path) {
|
||||
log::warn!("CreatedLayer overrides existing layer metadata.");
|
||||
}
|
||||
self.layer_metadata.insert(path.clone(), LayerMetadata::new(false));
|
||||
|
||||
responses.push_back(LayerChanged(path.clone()).into());
|
||||
self.layer_range_selection_reference = path.clone();
|
||||
responses.push_back(SetSelectedLayers(vec![path.clone()]).into());
|
||||
responses.push_back(AddSelectedLayers(vec![path.clone()]).into());
|
||||
}
|
||||
DocumentResponse::DocumentChanged => responses.push_back(RenderDocument.into()),
|
||||
};
|
||||
|
|
@ -923,6 +932,13 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||
}
|
||||
MoveSelectedLayersTo { path, insert_index } => {
|
||||
let layers = self.selected_layers().collect::<Vec<_>>();
|
||||
|
||||
// Trying to insert into self.
|
||||
if layers.iter().any(|layer| path.starts_with(layer)) {
|
||||
return;
|
||||
}
|
||||
let insert_index = self.update_insert_index(&layers, &path, insert_index).unwrap();
|
||||
responses.push_back(DocumentsMessage::Copy(Clipboard::System).into());
|
||||
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
|
||||
responses.push_back(
|
||||
|
|
@ -954,15 +970,11 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
if let Some(insert_path) = insert {
|
||||
let (id, path) = insert_path.split_last().expect("Can't move the root folder");
|
||||
if let Some(folder) = self.graphene_document.layer(path).ok().and_then(|layer| layer.as_folder().ok()) {
|
||||
let selected: Vec<_> = selected_layers
|
||||
.iter()
|
||||
.filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1)
|
||||
.map(|x| x.last().unwrap())
|
||||
.collect();
|
||||
let non_selected: Vec<_> = folder.layer_ids.iter().filter(|id| selected.iter().all(|x| x != id)).collect();
|
||||
let offset = if relative_position < 0 || non_selected.is_empty() { 0 } else { 1 };
|
||||
let fallback = offset * (non_selected.len());
|
||||
let insert_index = non_selected.iter().position(|x| *x == id).map(|x| x + offset).unwrap_or(fallback) as isize;
|
||||
let layer_index = folder.layer_ids.iter().position(|comparison_id| comparison_id == id).unwrap() as isize;
|
||||
|
||||
// If moving down, insert below this layer, if moving up, insert above this layer
|
||||
let insert_index = if relative_position < 0 { layer_index } else { layer_index + 1 };
|
||||
|
||||
responses.push_back(DocumentMessage::MoveSelectedLayersTo { path: path.to_vec(), insert_index }.into());
|
||||
}
|
||||
}
|
||||
|
|
@ -1029,42 +1041,6 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
}
|
||||
}
|
||||
RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()),
|
||||
MoveLayerInTree {
|
||||
layer: target_layer,
|
||||
insert_above,
|
||||
neighbor,
|
||||
} => {
|
||||
let neighbor_id = neighbor.last().expect("Tried to move next to root");
|
||||
let neighbor_path = &neighbor[..neighbor.len() - 1];
|
||||
|
||||
if !neighbor.starts_with(&target_layer) {
|
||||
let containing_folder = self.graphene_document.folder(neighbor_path).expect("Neighbor does not exist");
|
||||
let neighbor_index = containing_folder.position_of_layer(*neighbor_id).expect("Neighbor layer does not exist");
|
||||
|
||||
let layer = self.graphene_document.layer(&target_layer).expect("Layer moving does not exist.").to_owned();
|
||||
let destination_path = [neighbor_path.to_vec(), vec![generate_uuid()]].concat();
|
||||
let insert_index = if insert_above { neighbor_index } else { neighbor_index + 1 } as isize;
|
||||
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
responses.push_back(
|
||||
DocumentOperation::InsertLayer {
|
||||
layer,
|
||||
destination_path: destination_path.clone(),
|
||||
insert_index,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(
|
||||
DocumentMessage::UpdateLayerMetadata {
|
||||
layer_path: destination_path,
|
||||
layer_metadata: *self.layer_metadata(&target_layer),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(DocumentOperation::DeleteLayer { path: target_layer }.into());
|
||||
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||
}
|
||||
}
|
||||
SetSnapping(new_status) => {
|
||||
self.snapping_enabled = new_status;
|
||||
}
|
||||
|
|
@ -1094,7 +1070,6 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
|||
SaveDocument,
|
||||
SetSnapping,
|
||||
DebugPrintDocument,
|
||||
MoveLayerInTree,
|
||||
ZoomCanvasToFitAll,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -379,6 +379,7 @@ impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHa
|
|||
.graphene_document
|
||||
.shallowest_common_folder(document.selected_layers())
|
||||
.expect("While pasting, the selected layers did not exist while attempting to find the appropriate folder path for insertion");
|
||||
responses.push_back(DeselectAllLayers.into());
|
||||
responses.push_back(StartTransaction.into());
|
||||
responses.push_back(
|
||||
PasteIntoFolder {
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@
|
|||
</PopoverButton>
|
||||
</LayoutRow>
|
||||
<LayoutRow :class="'layer-tree scrollable-y'">
|
||||
<LayoutCol :class="'list'" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="updateInsertLine($event)" @dragend="drop()">
|
||||
<div class="layer-row" v-for="(layer, index) in layers" :key="String(layer.path.slice(-1))">
|
||||
<LayoutCol :class="'list'" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="updateInsertLine($event)" @dragend="drop($event)">
|
||||
<div class="layer-row" v-for="({ entry: layer }, index) in layers" :key="String(layer.path.slice(-1))">
|
||||
<div class="visibility">
|
||||
<IconButton
|
||||
:action="(e) => (toggleLayerVisibility(layer.path), e && e.stopPropagation())"
|
||||
|
|
@ -307,12 +307,12 @@ export default defineComponent({
|
|||
opacityNumberInputDisabled: true,
|
||||
// TODO: replace with BigUint64Array as index
|
||||
layerCache: new Map() as Map<string, LayerPanelEntry>,
|
||||
layers: [] as LayerPanelEntry[],
|
||||
layers: [] as { folderIndex: number; entry: LayerPanelEntry }[],
|
||||
layerDepths: [] as number[],
|
||||
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
|
||||
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
|
||||
opacity: 100,
|
||||
draggingData: undefined as undefined | { path: BigUint64Array; above: boolean; nearestPath: BigUint64Array; insertLine: HTMLDivElement },
|
||||
draggingData: undefined as undefined | { insertFolder: BigUint64Array; insertIndex: number; insertLine: HTMLDivElement },
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -343,32 +343,32 @@ export default defineComponent({
|
|||
},
|
||||
async clearSelection() {
|
||||
this.layers.forEach((layer) => {
|
||||
layer.layer_metadata.selected = false;
|
||||
layer.entry.layer_metadata.selected = false;
|
||||
});
|
||||
},
|
||||
closest(tree: HTMLElement, clientY: number): [BigUint64Array, boolean, Node] {
|
||||
closest(tree: HTMLElement, clientY: number): { insertFolder: BigUint64Array; insertIndex: number; insertAboveNode: Node } {
|
||||
const treeChildren = tree.children;
|
||||
|
||||
// Closest distance to the middle of the row along the Y axis
|
||||
let closest = Infinity;
|
||||
|
||||
// The nearest row parent (element of the tree)
|
||||
let nearestElement = tree.lastChild as Node;
|
||||
let insertAboveNode = tree.lastChild as Node;
|
||||
|
||||
// The nearest element in the path to the mouse
|
||||
let nearestPath = new BigUint64Array();
|
||||
// Folder to insert into
|
||||
let insertFolder = new BigUint64Array();
|
||||
|
||||
// Item goes above or below the mouse
|
||||
let above = false;
|
||||
// Insert index
|
||||
let insertIndex = -1;
|
||||
|
||||
Array.from(treeChildren).forEach((treeChild) => {
|
||||
if (treeChild.childElementCount <= 2) return;
|
||||
|
||||
const child = treeChild.children[2] as HTMLElement;
|
||||
const layerComponents = treeChild.getElementsByClassName("layer");
|
||||
if (layerComponents.length !== 1) return;
|
||||
const child = layerComponents[0];
|
||||
|
||||
const indexAttribute = child.getAttribute("data-index");
|
||||
if (!indexAttribute) return;
|
||||
const layer = this.layers[parseInt(indexAttribute, 10)];
|
||||
const { folderIndex, entry: layer } = this.layers[parseInt(indexAttribute, 10)];
|
||||
|
||||
const rect = child.getBoundingClientRect();
|
||||
const position = rect.top + rect.height / 2;
|
||||
|
|
@ -376,30 +376,31 @@ export default defineComponent({
|
|||
|
||||
// Inserting above current row
|
||||
if (distance > 0 && distance < closest) {
|
||||
insertAboveNode = treeChild;
|
||||
insertFolder = layer.path.slice(0, layer.path.length - 1);
|
||||
insertIndex = folderIndex;
|
||||
closest = distance;
|
||||
nearestPath = layer.path;
|
||||
above = true;
|
||||
if (child.parentNode) {
|
||||
nearestElement = child.parentNode;
|
||||
}
|
||||
}
|
||||
// Inserting below current row
|
||||
else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0 && layer.layer_type !== "Folder") {
|
||||
closest = -distance;
|
||||
nearestPath = layer.path;
|
||||
else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0) {
|
||||
if (child.parentNode && child.parentNode.nextSibling) {
|
||||
nearestElement = child.parentNode.nextSibling;
|
||||
insertAboveNode = child.parentNode.nextSibling;
|
||||
}
|
||||
insertFolder = layer.layer_type === "Folder" ? layer.path : layer.path.slice(0, layer.path.length - 1);
|
||||
insertIndex = layer.layer_type === "Folder" ? 0 : folderIndex + 1;
|
||||
closest = -distance;
|
||||
}
|
||||
// Inserting with no nesting at the end of the panel
|
||||
else if (closest === Infinity) {
|
||||
nearestPath = layer.path.slice(0, 1);
|
||||
else if (closest === Infinity && layer.path.length === 1) {
|
||||
insertIndex = folderIndex + 1;
|
||||
}
|
||||
});
|
||||
|
||||
return [nearestPath, above, nearestElement];
|
||||
return { insertFolder, insertIndex, insertAboveNode };
|
||||
},
|
||||
async dragStart(event: DragEvent, layer: LayerPanelEntry) {
|
||||
if (!layer.layer_metadata.selected) this.selectLayer(layer, event.ctrlKey, event.shiftKey);
|
||||
|
||||
// Set style of cursor for drag
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
|
|
@ -413,31 +414,30 @@ export default defineComponent({
|
|||
insertLine.classList.add("insert-mark");
|
||||
tree.appendChild(insertLine);
|
||||
|
||||
const [nearestPath, above, nearestElement] = this.closest(tree, event.clientY);
|
||||
const { insertFolder, insertIndex, insertAboveNode } = this.closest(tree, event.clientY);
|
||||
|
||||
// Set the initial state of the insert line
|
||||
if (nearestElement.parentNode) {
|
||||
insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * nearestPath.length}px`; // TODO: use layerIndent function to calculate this
|
||||
tree.insertBefore(insertLine, nearestElement);
|
||||
if (insertAboveNode.parentNode) {
|
||||
insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * (insertFolder.length + 1)}px`; // TODO: use layerIndent function to calculate this
|
||||
tree.insertBefore(insertLine, insertAboveNode);
|
||||
}
|
||||
|
||||
this.draggingData = { path: layer.path, above, nearestPath, insertLine };
|
||||
this.draggingData = { insertFolder, insertIndex, insertLine };
|
||||
},
|
||||
updateInsertLine(event: DragEvent) {
|
||||
// Stop the drag from being shown as cancelled
|
||||
event.preventDefault();
|
||||
|
||||
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement;
|
||||
|
||||
const [nearestPath, above, nearestElement] = this.closest(tree, event.clientY);
|
||||
const { insertFolder, insertIndex, insertAboveNode } = this.closest(tree, event.clientY);
|
||||
|
||||
if (this.draggingData) {
|
||||
this.draggingData.nearestPath = nearestPath;
|
||||
this.draggingData.above = above;
|
||||
this.draggingData.insertFolder = insertFolder;
|
||||
this.draggingData.insertIndex = insertIndex;
|
||||
|
||||
if (nearestElement.parentNode) {
|
||||
this.draggingData.insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * nearestPath.length}px`;
|
||||
tree.insertBefore(this.draggingData.insertLine, nearestElement);
|
||||
if (insertAboveNode.parentNode) {
|
||||
this.draggingData.insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * (insertFolder.length + 1)}px`;
|
||||
tree.insertBefore(this.draggingData.insertLine, insertAboveNode);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -448,12 +448,15 @@ export default defineComponent({
|
|||
},
|
||||
async drop() {
|
||||
this.removeLine();
|
||||
|
||||
if (this.draggingData) {
|
||||
this.editor.instance.move_layer_in_tree(this.draggingData.path, this.draggingData.above, this.draggingData.nearestPath);
|
||||
const { insertFolder, insertIndex } = this.draggingData;
|
||||
|
||||
this.editor.instance.move_layer_in_tree(insertFolder, insertIndex);
|
||||
}
|
||||
},
|
||||
setBlendModeForSelectedLayers() {
|
||||
const selected = this.layers.filter((layer) => layer.layer_metadata.selected);
|
||||
const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected);
|
||||
|
||||
if (selected.length < 1) {
|
||||
this.blendModeSelectedIndex = 0;
|
||||
|
|
@ -462,8 +465,8 @@ export default defineComponent({
|
|||
}
|
||||
this.blendModeDropdownDisabled = false;
|
||||
|
||||
const firstEncounteredBlendMode = selected[0].blend_mode;
|
||||
const allBlendModesAlike = !selected.find((layer) => layer.blend_mode !== firstEncounteredBlendMode);
|
||||
const firstEncounteredBlendMode = selected[0].entry.blend_mode;
|
||||
const allBlendModesAlike = !selected.find((layer) => layer.entry.blend_mode !== firstEncounteredBlendMode);
|
||||
|
||||
if (allBlendModesAlike) {
|
||||
this.blendModeSelectedIndex = this.blendModeEntries.flat().findIndex((entry) => entry.value === firstEncounteredBlendMode);
|
||||
|
|
@ -474,7 +477,7 @@ export default defineComponent({
|
|||
},
|
||||
setOpacityForSelectedLayers() {
|
||||
// todo figure out why this is here
|
||||
const selected = this.layers.filter((layer) => layer.layer_metadata.selected);
|
||||
const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected);
|
||||
|
||||
if (selected.length < 1) {
|
||||
this.opacity = 100;
|
||||
|
|
@ -483,8 +486,8 @@ export default defineComponent({
|
|||
}
|
||||
this.opacityNumberInputDisabled = false;
|
||||
|
||||
const firstEncounteredOpacity = selected[0].opacity;
|
||||
const allOpacitiesAlike = !selected.find((layer) => layer.opacity !== firstEncounteredOpacity);
|
||||
const firstEncounteredOpacity = selected[0].entry.opacity;
|
||||
const allOpacitiesAlike = !selected.find((layer) => layer.entry.opacity !== firstEncounteredOpacity);
|
||||
|
||||
if (allOpacitiesAlike) {
|
||||
this.opacity = firstEncounteredOpacity;
|
||||
|
|
@ -497,14 +500,14 @@ export default defineComponent({
|
|||
mounted() {
|
||||
this.editor.dispatcher.subscribeJsMessage(DisplayFolderTreeStructure, (displayFolderTreeStructure) => {
|
||||
const path = [] as bigint[];
|
||||
this.layers = [] as LayerPanelEntry[];
|
||||
this.layers = [] as { folderIndex: number; entry: LayerPanelEntry }[];
|
||||
|
||||
const recurse = (folder: DisplayFolderTreeStructure, layers: LayerPanelEntry[], cache: Map<string, LayerPanelEntry>): void => {
|
||||
folder.children.forEach((item) => {
|
||||
const recurse = (folder: DisplayFolderTreeStructure, layers: { folderIndex: number; entry: LayerPanelEntry }[], cache: Map<string, LayerPanelEntry>): void => {
|
||||
folder.children.forEach((item, index) => {
|
||||
// TODO: fix toString
|
||||
path.push(BigInt(item.layerId.toString()));
|
||||
const mapping = cache.get(path.toString());
|
||||
if (mapping) layers.push(mapping);
|
||||
if (mapping) layers.push({ folderIndex: index, entry: mapping });
|
||||
if (item.children.length >= 1) recurse(item, layers, cache);
|
||||
path.pop();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -383,8 +383,8 @@ impl JsEditorHandle {
|
|||
}
|
||||
|
||||
/// Move a layer to be next to the specified neighbor
|
||||
pub fn move_layer_in_tree(&self, layer: Vec<LayerId>, insert_above: bool, neighbor: Vec<LayerId>) {
|
||||
let message = DocumentMessage::MoveLayerInTree { layer, insert_above, neighbor };
|
||||
pub fn move_layer_in_tree(&self, path: Vec<LayerId>, insert_index: isize) {
|
||||
let message = DocumentMessage::MoveSelectedLayersTo { path, insert_index };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue