Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix select all and change ordinal in edit mode in io #3109

Merged
merged 9 commits into from
Apr 11, 2024

Conversation

krmanik
Copy link
Contributor

@krmanik krmanik commented Apr 1, 2024

The edit mode needs careful review, I have come up with the solution, but it may need improvement.
@abdnh pinging for review.

Closes #3109

@dae
Copy link
Member

dae commented Apr 1, 2024

I gave this a quick test, and it seems to fix the problem for me in both edit mode and the add screen.

@krmanik
Copy link
Contributor Author

krmanik commented Apr 1, 2024

Initial test works, but after adding some notes with group mode then again in edit mode, ungroup last shapes then group some other shapes. The ordinal seems to change and causing error something like No cloze found on number.

@krmanik krmanik force-pushed the fix-io-group-tool branch from dc0eb14 to 49d6c38 Compare April 1, 2024 12:33
@krmanik
Copy link
Contributor Author

krmanik commented Apr 1, 2024

The solution after changing group item can be fixed by introducing a new method which checks for ordinal number in cloze strings and reorder, the issue also fixed by Tools -> Empty Cards. The complicated solution may be reduced by just using just ordinal = 0, but some users may want to have same ordinal in edit mode for other shapes.

Edit:
Neither solution going to work, because before group total card in note type is less but when ungroup the cards increased but again grouping does not decrease total cards. The issue needs to be fix separately.

@dae
Copy link
Member

dae commented Apr 2, 2024

Sorry Mani, could I trouble you to explain the issue in more detail? If a user reduces the number of occlusions, I'd expect that to break one or more of their cards. It would be nice if we could display a useful message during review in that case, like we do when there's an empty cloze. But I don't think we need to be cleaning up the cards until the user uses Tools>Empty Cards.

@krmanik
Copy link
Contributor Author

krmanik commented Apr 2, 2024

In this demo, first I have created group shapes cloze, then ungroup it in edit mode, after that again group the shapes, it needs to remove empty cards.
We may show popup that after group and ungroup some cards have been updated and it needs to clear empty cards.

output.mp4

@krmanik
Copy link
Contributor Author

krmanik commented Apr 3, 2024

The ordinal is undefined for all shapes when group/ungroup tools used, so that the cloze number should be in order.

@dae
Copy link
Member

dae commented Apr 3, 2024

Hmm, this doesn't seem to work - if I add two rectangles to a new card, then edit the card and select all+group, the two shapes stay disconnected when the editor is closed.

@krmanik
Copy link
Contributor Author

krmanik commented Apr 3, 2024

Now it will work as expected, but the users still have to use Tools -> Empty Cards.


I have used, to fix I have to use rm -rf out node_modules

PS C:\Users\mani\Documents\GitHub\anki> .\tools\ninja.bat format
....
PS C:\Users\mani\Documents\GitHub\anki> .\tools\ninja.bat check 
    Finished release [optimized] target(s) in 0.43s
[1/1; 1 active; 7.639s] build:configureninja: error: 'node_modules/@sveltejs/kit/src/types/synthetic/+dynamic+private.md', needed by 'out/tests/dprint.check', missing and no known rule to make it

@dae
Copy link
Member

dae commented Apr 3, 2024

I've pushed a fix for the dprint error. Tests aren't currently passing, but seems to work well when I tried the grouping operation again with the latest code.

@krmanik krmanik force-pushed the fix-io-group-tool branch 2 times, most recently from 5ea6334 to 49cd788 Compare April 3, 2024 08:15
@krmanik
Copy link
Contributor Author

krmanik commented Apr 3, 2024

I've pushed a fix for the dprint error. Tests aren't currently passing, but seems to work well when I tried the grouping operation again with the latest code.

Thanks, I have rebased it to main.

@@ -166,7 +168,7 @@ function shapeOrShapesToCloze(

// Maintain existing ordinal in editing mode
let ordinal = shapeOrShapes.ordinal;
if (ordinal === undefined) {
if (ordinal === undefined || get(groupItemsModified)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can break the ordinals of ungrouped masks when a grouping operation is done on other masks in edit mode.

Can we instead update the ordinals of grouped objects when they are grouped?
For example, If we group objects with the ordinals 2 and 4, all objects in the group will be assigned a single ordinal (maybe the smallest one, so 2).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like a good way of decreasing the number of empty cards we end up producing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, because let's say a user have created group cloze with more than two shapes as single group cloze, then during review he edited the shape to ungroup to individual shapes. Now total cards increased from one to total number of shapes. Now again during review he split it into two group, now ordinal must start from beginning to have consistent result, (Card 1 and Card 2)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can solve this for all situations, especially if the user is closing and opening the editor between changes. I'd assumed that if we give new ordinals to shapes when they're removed from a group, that would be sufficient?

Copy link
Contributor Author

@krmanik krmanik Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The solution in this PR should be accepted because if user modified the group items, there needs to implement to change ordinal for group items only, needs to keep track of group items, so instead of handling those cases, we should warn user that group and ungroup of shapes leads to changes of ordinal so refrain from doing this. But if no group shapes are touched the ordinal also does not modified.

I'd assumed that if we give new ordinals to shapes when they're removed from a group, that would be sufficient?

I am implementing and testing it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to check we're on the same page - would you like this PR merged in without the changes that Abdo suggested?

Copy link
Contributor Author

@krmanik krmanik Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have thought of implementation idea for Abdo suggestion, I have implemented it in next commit, and it seems, it also solves the empty card issues. But it still needs some review.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users need to do Tools -> Empty Cards, I have tried to solve but once cards added, the ordinal decreases but Cards remains same, so it needs to be fixed separately.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm not quite following - what do you think needs to be done differently?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing, I was implementing different solution when ungroup shapes. In case of ungroup shapes, find all the ordinal from shapes excluding groups shapes then start loop from beginning and put the ordinal to shapes in order. But I implemented simplest solution, use max ordinal (increment if required).

@krmanik krmanik force-pushed the fix-io-group-tool branch 2 times, most recently from d330ed2 to 05bd940 Compare April 3, 2024 14:06
@@ -63,12 +63,19 @@ export const groupShapes = (canvas: fabric.Canvas): void => {

const activeObject = canvas.getActiveObject() as fabric.ActiveSelection;
const items = activeObject.getObjects();

// @ts-expect-error not defined
const minOrdinal = Math.min(...items.map((item) => item.ordinal));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something that might be nice in the future (not in this PR) is to tell TypeScript about the additional properties we've added to these objects, so we don't need to ignore errors/cast them to any each time we access them. I think it may be possible in a .d.ts file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be done, I will look into this.

@dae
Copy link
Member

dae commented Apr 4, 2024

Thanks Mani, this seems to work well. @abdnh, any thoughts/comments? If not, I'll merge this in tomorrow and get a 24.04.1rc out.


items.forEach((item, index) => {
// @ts-expect-error not defined
item.set({ opacity: get(opacityStateStore) ? 0.4 : 1, ordinal: maxOrdinal + index + 1 });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not working as expected. It can result in two unrelated masks getting the same ordinal, causing them to be grouped. To reproduce:

  1. Add 3 masks
  2. Group two of the masks
  3. Ungroup
  4. Reload the note (e.g. by selecting another browser note then back to the note)
  5. Notice one mask from the original group is grouped with the third.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the implementation, it should work now, but order cannot be maintained in case of ungrouping of shapes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching that Abdo! And thank you Mani for your continued work on this. I gave it a quick try again and couldn't seem to trip it up. When you say the order can't be maintained, I wonder if that's a problem? As long as we're not reusing ordinals, what problems do you think it would cause?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, we can go with this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dae While the issue I reported is fixed, this is re-introducing the original problem fixed by #2987 when there's an ungroup operation. Unrelated masks could get reordered.

This appears to work better:

diff --git a/ts/routes/image-occlusion/shapes/to-cloze.ts b/ts/routes/image-occlusion/shapes/to-cloze.ts
index 01d8b62c9..fbdd03f07 100644
--- a/ts/routes/image-occlusion/shapes/to-cloze.ts
+++ b/ts/routes/image-occlusion/shapes/to-cloze.ts
@@ -3,9 +3,7 @@

 import { fabric } from "fabric";
 import { cloneDeep } from "lodash-es";
-import { get } from "svelte/store";

-import { shapeUngroupState } from "../store";
 import { getBoundingBox } from "../tools/lib";
 import type { Size } from "../types";
 import type { Shape, ShapeOrShapes } from "./base";
@@ -22,6 +20,7 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): {

     let clozes = "";
     let index = 0;
+    let nextOrdinal = 1;
     shapes.forEach((shapeOrShapes) => {
         // shapes with width or height less than 5 are not valid
         if (shapeOrShapes === null) {
@@ -31,7 +30,33 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): {
         if (shapeOrShapes instanceof Rectangle && shapeOrShapes.fill === "transparent") {
             return;
         }
-        clozes += shapeOrShapesToCloze(shapeOrShapes, index, occludeInactive);
+        // Maintain existing ordinal in editing mode
+        let ordinal: number | undefined;
+        if (Array.isArray(shapeOrShapes)) {
+            ordinal = shapeOrShapes[0].ordinal;
+        } else {
+            ordinal = shapeOrShapes.ordinal;
+        }
+        if (ordinal === undefined) {
+            if (shapeOrShapes instanceof Text) {
+                ordinal = 0;
+            } else {
+                ordinal = nextOrdinal;
+            }
+            if (Array.isArray(shapeOrShapes)) {
+                shapeOrShapes.forEach((shape) => (shape.ordinal = ordinal));
+            } else {
+                shapeOrShapes.ordinal = ordinal;
+            }
+        }
+        nextOrdinal = ordinal + 1;
+
+        clozes += shapeOrShapesToCloze(
+            shapeOrShapes,
+            index,
+            ordinal,
+            occludeInactive,
+        );
         if (!(shapeOrShapes instanceof Text)) {
             index++;
         }
@@ -134,6 +159,7 @@ function fabricObjectToBaseShapeOrShapes(
 function shapeOrShapesToCloze(
     shapeOrShapes: ShapeOrShapes,
     index: number,
+    ordinal: number,
     occludeInactive: boolean,
 ): string {
     let text = "";
@@ -145,7 +171,7 @@ function shapeOrShapesToCloze(
     let type: string;
     if (Array.isArray(shapeOrShapes)) {
         return shapeOrShapes
-            .map((shape) => shapeOrShapesToCloze(shape, index, occludeInactive))
+            .map((shape) => shapeOrShapesToCloze(shape, index, ordinal, occludeInactive))
             .join("");
     } else if (shapeOrShapes instanceof Rectangle) {
         type = "rect";
@@ -166,16 +192,6 @@ function shapeOrShapesToCloze(
         addKeyValue("oi", "1");
     }

-    // Maintain existing ordinal in editing mode
-    let ordinal = shapeOrShapes.ordinal;
-    if (ordinal === undefined || Number.isNaN(ordinal) || get(shapeUngroupState)) {
-        if (type === "text") {
-            ordinal = 0;
-        } else {
-            ordinal = index + 1;
-        }
-        shapeOrShapes.ordinal = ordinal;
-    }
     text = `{{c${ordinal}::image-occlusion:${type}${text}}}<br>`;

     return text;
diff --git a/ts/routes/image-occlusion/store.ts b/ts/routes/image-occlusion/store.ts
index b1653f4fa..5847250dd 100644
--- a/ts/routes/image-occlusion/store.ts
+++ b/ts/routes/image-occlusion/store.ts
@@ -17,5 +17,3 @@ export const ioImageLoadedStore = writable(false);
 export const opacityStateStore = writable(false);
 // store state of text editing
 export const textEditingState = writable(false);
-// store state of shape ungroup
-export const shapeUngroupState = writable(false);
diff --git a/ts/routes/image-occlusion/tools/lib.ts b/ts/routes/image-occlusion/tools/lib.ts
index 9a6738b10..bc0606cdc 100644
--- a/ts/routes/image-occlusion/tools/lib.ts
+++ b/ts/routes/image-occlusion/tools/lib.ts
@@ -4,7 +4,7 @@
 import { fabric } from "fabric";
 import { get } from "svelte/store";

-import { opacityStateStore, shapeUngroupState } from "../store";
+import { opacityStateStore } from "../store";

 export const SHAPE_MASK_COLOR = "#ffeba2";
 export const BORDER_COLOR = "#212121";
@@ -94,11 +94,14 @@ export const unGroupShapes = (canvas: fabric.Canvas): void => {
     canvas.remove(group);

     items.forEach((item) => {
-        item.set({ opacity: get(opacityStateStore) ? 0.4 : 1 });
+        item.set({
+            opacity: get(opacityStateStore) ? 0.4 : 1,
+            // @ts-expect-error not defined
+            ordinal: undefined,
+        });
         canvas.add(item);
     });

-    shapeUngroupState.set(true);
     redraw(canvas);
 };

What do you think @dae @krmanik ?

Copy link
Contributor Author

@krmanik krmanik Apr 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit:

It needs to check Number.isNaN(ordinal), working now. Thanks Abdo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch @abdnh! And thanks for the quick fix @krmanik.

Unfortunately I ran into another issue. I created three rectangles and merged them into a group, then created three more, and left them ungrouped. I added the note. Then I edited it, and ungrouped the three rectangles. Two surprises:

  • The group's ordinal was not reused, so the first card is now empty and needs to be cleaned up with tools>empty cards
  • One of the shapes in the group was given the same ordinal as one of the shapes that was never in a group, so it ended up in a new group.

@krmanik krmanik force-pushed the fix-io-group-tool branch from 05bd940 to 2511c57 Compare April 5, 2024 03:15
@krmanik
Copy link
Contributor Author

krmanik commented Apr 5, 2024

I was trying to implement this, but it works sometimes and sometimes does not, but the simplest solution will be just use svelte store only for ungroup shapes which works now.

Code
diff --git a/ts/routes/image-occlusion/tools/lib.ts b/ts/routes/image-occlusion/tools/lib.ts
index f7d00846c..38fccd703 100644
--- a/ts/routes/image-occlusion/tools/lib.ts
+++ b/ts/routes/image-occlusion/tools/lib.ts
@@ -93,18 +93,53 @@ export const unGroupShapes = (canvas: fabric.Canvas): void => {
     group.destroyed = true;
     canvas.remove(group);
 
-    // @ts-expect-error not defined
-    const maxOrdinal = Math.max(canvas.getObjects().map((item) => item.ordinal));
+    // take ordinals for all shape excluding the ones in the group
+    const excludedObjectOrdinals = getCanvasObjects(canvas)
+        .filter((obj) => !items.includes(obj))
+        .map((item) => item.ordinal)
+        .filter((ordinal) => ordinal !== undefined || !Number.isNaN(ordinal));
+
+    console.log("excludedObjectOrdinals", excludedObjectOrdinals);
+
+    const totalShapes = getCanvasObjects(canvas).length;
+
+    console.log("totalShapes", totalShapes);
+
+    const newOrdinal = [];
+    for (let i = 1; i < totalShapes; i++) {
+        if (!excludedObjectOrdinals.includes(i)) {
+            newOrdinal.push(i);
+        }
+    }
+
+    console.log("newOrdinal", newOrdinal);
 
     items.forEach((item, index) => {
         // @ts-expect-error not defined
-        item.set({ opacity: get(opacityStateStore) ? 0.4 : 1, ordinal: maxOrdinal + index + 1 });
+        item.set({ opacity: get(opacityStateStore) ? 0.4 : 1, ordinal: newOrdinal[index] });
         canvas.add(item);
     });
 
     redraw(canvas);
 };
 
+const getCanvasObjects = (canvas) => {
+    const objectList = [];
+    canvas.forEachObject(function(object) {
+        if (object.type === 'group') {
+            const group = object;
+            group.forEachObject(function(obj) {
+                objectList.push(obj);
+            });
+        } else {
+            objectList.push(object);
+        }
+    });
+
+    // filter out the transparent bounding box from the selection
+    return objectList.filter((obj) => obj.fill !== "transparent");
+}
+
 const copyItem = (canvas: fabric.Canvas): void => {
     const activeObject = canvas.getActiveObject();
     if (!activeObject) {

@dae
Copy link
Member

dae commented Apr 5, 2024

@abdnh any other thoughts, or happy for this to be merged in?

@krmanik krmanik force-pushed the fix-io-group-tool branch from 2511c57 to d134dde Compare April 7, 2024 02:54
@@ -21,6 +21,9 @@ export function exportShapesToClozeDeletions(occludeInactive: boolean): {
let clozes = "";
let index = 0;
let nextOrdinal = 1;

console.log(shapes);
Copy link
Contributor Author

@krmanik krmanik Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this console log seems to fix the issue, so is the issue race condition? Then may be we should use promise and resolve or settimeout. (It works when Ankiweb inspect opened)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid for me, the same issue still happens when I update to the latest commit which has this console.log call. I tried 3 times, and it happened on the 1st and 2nd time, but not the 3rd. If you try a few more times, does it ever happen? Could it be something other than a race condition, such as unpredictable dictionary ordering?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand the underlying issues, so I have changed the implementation, it works as expected. The general idea is takeout ordinal which does not assign to any shape from a list of ordinal and assign to shape.

@krmanik krmanik force-pushed the fix-io-group-tool branch from cf431a7 to 458c9e1 Compare April 10, 2024 10:22
@krmanik
Copy link
Contributor Author

krmanik commented Apr 10, 2024

The order of shapes in group issue is resolved now in latest commit.

@dae
Copy link
Member

dae commented Apr 10, 2024 via email

@dae
Copy link
Member

dae commented Apr 11, 2024

Thanks Mani, this seems to work well, and if there are any issues with the code, I didn't notice them. ^_^; I'm going to merge this in now so I can get a beta out today, but if you have any concerns @abdnh, please let us know.

@dae dae merged commit 477f932 into ankitects:main Apr 11, 2024
1 check passed
dae added a commit that referenced this pull request Apr 11, 2024
* fix select all and change ordinal in edit mode in io

* make ordinal undefined for all shapes in group/ungroup

* fix group shapes and some ui fixes

* Don't add node_modules/* to dprint deps

* use minimum ordinal when shape merged, use max ordinal++ when ungrouped, in add mode no ordinal preset so NaN

* use state for ungroup shape

* maintain existing ordinal in editing mode

* fix order of ordinal in ungroup shape

* refactor: remove for loop, use forEach

(cherry picked from commit 477f932)
@abdnh
Copy link
Collaborator

abdnh commented Apr 11, 2024

Looking good to me. Didn't notice any issues either.

@krmanik krmanik deleted the fix-io-group-tool branch April 12, 2024 02:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants