Lulucat

שני מחקים, ולמה המחיקה שייכת למשיכה

Gaoge ZhangGaoge Zhang

משיכת מחק כאלמנט עצמאי היא התכנון המתבקש. הזזת אובייקטים שוברת אותו, ולכן ב-Lulucat Notes המחיקה שייכת למשיכה שממנה היא מסירה את הדיו.

עודכן

כאשר מוחקים במחברת, מתכוונים לשני דברים שונים. לפעמים סימן שגוי וכל הסימן צריך להיעלם: קו עזר, מילה שסומנה בקו חוצה, משיכה תועה מכף יד שנחה על הדף. לפעמים רק חלק מסימן שגוי: חצי תו, ענף אחד בתרשים, זנב של משיכת מרקר שגלשה מעבר למשפט. Lulucat Notes מספקת כעת את שני הסוגים, כשני כלים נפרדים.

שאלת התכנון הקשה יותר היא היכן המחיקה שוכנת.

מחק המשיכות הגיע ראשון

המחק הראשון מחק משיכות שלמות. בכל נגיעה נבדקת כל משיכה: תיבות תוחמות דוחות כמעט את כולן, והמשיכות שעוברות את הסינון עוברות בדיקת מרחק בריבוע מנקודה למקטע, מול רדיוס המחק בתוספת רדיוס המשיכה שעבר אינטרפולציה לאורך המקטע. הרוחב משתנה לפי הלחץ, ולכן רוחב נומינלי יחיד היה מוחק משיכות דקות שהמשתמש לא נגע בהן ומפספס משיכות עבות שנגע בהן.

מחווה אחת מתמזגת לרשומת ביטול אחת, והרשומה שומרת כל משיכה שהוסרה יחד עם האינדקס המקורי שלה. ביטול מוסיף אותן בחזרה בסדר הפוך, ולכן סדר השכבות חוזר בדיוק למצבו הקודם.

המחק הזה מהיר, צפוי, ואינו מסוגל להסיר חצי מילה.

MaLiang הופך את המחק למברשת

MaLiang, מסגרת הציור ברישיון MIT שממנה כבר שאלנו את מודל חותמות המרקר, עונה על מחיקה חלקית בעשרים וארבע שורות. ה-Eraser שלה הוא תת-מחלקה של Brush שעוקפת דבר אחד בלבד, את אפשרויות המיזוג, כדי לחסר במקום להוסיף. משיכת מחק היא אלמנט באותה רשימה כמו כל משיכה אחרת. ביטול מסיר את האלמנט הזה. שום משיכה קיימת לא משתנה, כי המחיקה מתרחשת רק בזמן ההשמעה מחדש, לפי סדר הרשימה.

שחררנו את המודל הזה, ושני סשנים במכשירים שברו אותו.

הזזת אובייקטים חושפת את הבעלים החסר

הדיווח הראשון: בוחרים משיכות באמצעות הלאסו, גוררים אותן, והחור נשאר מאחור.

ארבעה פאנלים המשווים בין שני מודלים של מחק לפני ואחרי הזזת המשיכות. כשהמחק הוא אלמנט עצמאי, הפער נשאר במיקום הישן; כשהמחיקה בבעלות המשיכה, הפער נע יחד עם הדיו.

אותן משיכות שנמחקו, הוזזו ימינה. למעלה: המחק הוא אלמנט עצמאי. למטה: המחיקה שייכת למשיכות.

הבחירה החמירה את המצב. משיכות שנבחרו מורמות מתוך מפת הסיביות אל שכבת-על שמציירת אותן ישירות, בלי אלמנט המחק, ולכן ברגע שבחרתם משיכה שנמחקה היא נראתה שלמה שוב.

הדיווח השני: המחק ריחף מעל המרקר. המרנדר שלנו מנגן מחדש בשני מעברים, מרקרים תחילה ועטים אחר כך, כי דיו המרקר חייב להיות מתחת לדיו. אלמנט מחק חייב להשתתף בשני המעברים, אחרת הוא אינו יכול למחוק את שני הסוגים. השתתפות במעבר העטים פירושה הרצה אחרי שכל המרקרים צוירו, ולכן המחק חתך גם משיכות מרקר שצוירו מאוחר יותר בזמן.

לשני הכשלים יש אותו מקור. ב-MaLiang אין לאסו, אין הזזה ואין סדר שכבות בין מברשות, ולכן אלמנט ללא בעלים מספיק שם. לקנבס שלנו יש את שלושתם, ולמחיקה ללא בעלים אין תשובה מוגדרת לשאלה “מה קורה כשהדיו זז” או “על איזו שכבה הפעולה הזו מתבצעת”.

המחיקה שייכת למשיכה

כל אובייקט מסוג Stroke נושא כעת נתיבי מחיקה משלו. המחק בודק באילו משיכות הוא נוגע ומוסיף נתיב לכל אחת מהן, בקואורדינטות הקנבס. רינדור של משיכה אחת מצייר את הדיו שלה ואת המחיקות שלה בתוך שכבת שקיפות:

context.beginTransparencyLayer(auxiliaryInfo: nil)
drawInk(stroke, in: context, clip: clip, colorOverride: colorOverride)
context.setBlendMode(.destinationOut)
for path in stroke.erasePaths { fillErasePath(path, in: context) }
context.endTransparencyLayer()

Destination-out שומר על היעד ומסיר אותו ביחס לאלפא של המקור:

בתוך שכבת שקיפות היעד הוא אותה משיכה בלבד, ולכן הפעולה אינה יכולה להגיע אל הנייר, אל המשיכות שמתחתיה או אל המשיכות שצוירו אחריה. השכבה אינה מוסיפה עלות עבור משיכות ללא מחיקות, שהן כמעט כולן: הן עוברות בנתיב המקורי.

הבעלות עונה ישירות על שני הכשלים. הזזה מעבירה את נתיבי המחיקה יחד עם נקודות הדגימה, ולכן משיכה שהוזזה נושאת את הפערים שלה. שכבת-העל של הבחירה מציירת את אותו אובייקט Stroke, ולכן משיכה שנבחרה מציגה את אותם פערים. סדר השכבות מפסיק להיות חשוב, כי מחיקה לעולם אינה חלה על דבר מלבד המשיכה שלה.

הביטול פועל על המחווה: רשומה אחת מונה כל זוג (stroke, path) שהמחווה יצרה, ביטול מסיר את הנתיבים האלה, וביצוע מחדש מוסיף אותם שוב. הדיו שמתחת למחיקה לעולם אינו נערך, ולכן אין צורך לשחזר דבר.

משוב חי עובר דרך מפת הסיביות של האריחים

המשיכה שנמצאת בתהליך ציור נרנדרת בדרך כלל כשכבת-על מעל הדף המרוכב. מחיקה אינה יכולה לפעול שם, מפני ששכבת-העל יושבת מעל הנייר וגם מעל הדיו, ו-destination-out ינקב גם את הנייר.

לכן המחק כותב מיד לתוך המשיכות, וכל אצווה של דגימות חדשות מפעילה השמעה מחדש מקומית רק של המלבן המושפע. צינור האריחים כבר מנקה מלבן ומנגן מחדש את המשיכות שחוצות אותו; המחק משתמש שוב בנתיב הזה ואינו מוסיף סמנטיקה נוספת של ציור.

שני המחקים, זה לצד זה

סרגל הכלים כולל שני לחצני מחק במקום לחצן אחד עם מצב נסתר. המחק החלקי מסיר חלק ממשיכה. מחק המשיכות מסיר משיכות שלמות במגע. הרדיוסים שלהם הם קבועים נפרדים, ושניהם כרגע ארבע נקודות.

מחיקת משיכה שלמה מסירה איתה את נתיבי המחיקה, כי הם חלק מהמשיכה, וביטול מחזיר את שניהם יחד. נתיבי המחיקה הם גם חלק מהקובץ השמור, ומכומתים כמו כל השאר: הרדיוס והקואורדינטות נשמרים בעשיריות נקודה.

מגבלה אחת נותרה. משיכה שנמחקה לחלוטין עדיין קיימת כנתונים. היא אינה מופיעה ברינדור, אבל הלאסו עדיין יכול לבחור בה ומחק המשיכות עדיין יכול למחוק אותה, והיא עדיין תופסת מקום במסמך. איסוף משיכות שנמחקו לחלוטין דורש בדיקת כיסוי שטרם כתבנו, ולכן בינתיים הן נשארות.

כשמודל מפרויקט אחר מפסיק לעבוד אצלכם, ההבדל בדרך כלל אינו בקוד שהעתקתם. הוא נמצא בתכונות שאין באותו פרויקט.