للوحة على Mac تمتد عبر إحداثيات شاسعة وتفاصيل دقيقة، خزّن المواقع التقريبية منفصلة عن الإزاحات المحلية. اطرح أصل العرض قبل تطبيق التكبير، وافحص الدقة عند كل عملية تحويل. يشرح هذا الدليل الحساب باستخدام بيانات مشهد اصطناعية.
المهمة المحددة هي تحديد موضع علامة اصطناعية على لوحة سطح مكتب قابلة للتكبير من دون فقدان إزاحتها الصغيرة بسبب كبر عنوانها الإجمالي في المشهد. يتعامل المثال مع محور واحد؛ طبّق الحساب نفسه بشكل مستقل على المحور الآخر. وينتج إحداثيات رسم رقمية تتيح لك فحص العمليات الحسابية قبل ربطها بلوحة.
لماذا تختفي حركة صغيرة عند إحداثي كبير؟
Double في Swift نوع فاصلة عائمة بدقة 64 بت. يتيح نطاقه تمثيل مقادير كبيرة وصغيرة، لكن هذا لا يعني أنه يستطيع الاحتفاظ بكل زيادة صغيرة تُضاف إلى قيمة كبيرة. توثّق Apple النوع وواجهات التحويل الخاصة به في Double.
تحتوي دقة الأعداد الثنائية مزدوجة الدقة على 53 رقمًا ثنائيًا معنويًا. لذلك تزداد المسافة بين القيم القابلة للتمثيل مع ازدياد المقدار. يشرح مرجع Goldberg للحسابات هذا التمثيل وكيف يمكن لطرح قيمتين سبق تقريبهما أن يكشف عن معلومات فُقدت في مرحلة سابقة. ما ينبغي لكل عالم حاسوب معرفته عن حسابات الفاصلة العائمة
لنتأمل هذه المدخلات التوضيحية الصريحة: فهرس خلية مقداره 2^40، وعرض خلية يساوي 1024 وحدة مشهد، وإزاحة علامة مقدارها 2^-30 وحدة مشهد. يؤدي جمع عنوان الخلية وعرضها إلى إحداثي مطلق مقداره 2^50. عند هذا الإحداثي الموجب، تفصل 0.25 وحدة بين قيم Double المتجاورة. وتؤدي إضافة الإزاحة الصغيرة إلى تقريب القيمة مجددًا إلى الإحداثي الأصلي.
لذلك ينتج عن طرح الأصل بعد ذلك صفر. ولا يمكن لضرب هذا الصفر في عامل تكبير كبير استعادة الإزاحة. حدث الفقد عند تكوين الإحداثي المطلق.
استخدم x.ulp لفحص المسافة عند المقدار الذي تخزنه فعليًا. تعرّف Apple هذه الكمية بأنها وحدة المكانة الأخيرة، وتوثّق استثناءاتها، بما فيها القيم غير المنتهية. يصف Double.ulpOfOne المسافة عند الواحد؛ وليس هامش سماح مطلقًا عامًا لإحداثيات المشهد. ulp
ما الذي ينبغي أن يحافظ عليه تمثيل الإحداثيات؟
في هذه اللوحة، مثّل كل محور بفهرس خلية من نوع Int64 وإزاحة محلية من نوع Double. يعني الموضع مفاهيميًا cell × cellSize + local، لكن ينبغي أن يتجنب الحساب إنشاء هذه القيمة المطلقة ذات الفاصلة العائمة.
احتفظ بالعلامة وأصل العرض بهذا الشكل المنقسم منذ البداية. فتحويل إحداثي مطلق سبق تقريبه إلى خلية وإزاحة لا يمكنه استعادة التفاصيل المفقودة.
فيما يلي افتراضات التصميم المستخدمة في المثال، وليست حدودًا رقمية عامة:
- عرض كل خلية
1024وحدة مشهد بالضبط. - تستخدم فهارس الخلايا
Int64؛ فهي عناوين أو أعداد، وليست كميات ذات فاصلة عائمة. - كل إزاحة محلية منتهية وتقع ضمن
0..<1024. - لا يقبل تحديد الموضع الدقيق إلا خلية الأصل والخليتين المجاورتين مباشرةً.
- التكبير عدد موجب ومنتهٍ لوحدات العرض لكل وحدة مشهد.
- أصل الشاشة هو إحداثي الرسم الاصطناعي
512.
احسب الإحداثي النسبي بصيغة cellDifference × cellSize + localDifference. والأهم أن تطرح فهارس الخلايا الصحيحة قبل تحويل الفرق إلى Double. قد يقرّب التحويل الصحيح العادي في Apple؛ أما Double(exactly:) فيُخفق عندما يتعذر التمثيل الدقيق. Double
حتى طرح قيمتين صحيحتين صالحتين من نوع Int64 قد يتجاوز نطاق ذلك النوع. استخدم subtractingReportingOverflow(_:) وافحص علامته قبل استخدام النتيجة. توثّق Apple صراحةً أن الناتج الجزئي يُقتطع عند حدوث تجاوز. subtractingReportingOverflow(_:)
يحافظ هذا التخطيط على العنوان الكبير مستقلًا عن التفاصيل المحلية. لكن دقته تظل معتمدة على الإزاحات المحلية: فقد تختفي زيادة صغيرة جدًا عند إضافتها إلى إزاحة محلية كبيرة بما يكفي. اختر عروض الخلايا ونقاط الإسناد المحلية وفق أصغر تفصيل يحتاج إليه العرض الحالي.
نفّذ حسابًا محليًا كاملًا على Mac
تحتاج إلى سلسلة أدوات Swift عاملة. اتبع تعليمات تثبيت Swift — macOS الحالية عند الحاجة. في Terminal، تحقّق من التثبيت وأنشئ حزمة تنفيذية صغيرة باستخدام سير العمل الموثّق في إنشاء أداة لسطر الأوامر.
swift --version
mkdir SceneCoordinates
cd SceneCoordinates
swift package init --name SceneCoordinates --type executable
في هدف الملف التنفيذي الذي تم إنشاؤه ضمن Sources، استبدل ملف Swift الخاص بنقطة الدخول بالبرنامج الكامل أدناه. استخدم ملف نقطة دخول واحدًا يحتوي على هذا البرنامج، من دون عبارات إضافية على المستوى الأعلى. لا يستورد البرنامج إلا المكتبة القياسية في Swift ولا يحتاج إلى أي اعتماديات للحزمة.
import Swift
struct AxisPosition {
let cell: Int64
let local: Double
}
enum CoordinateError: Error {
case invalidInput
case cellDifferenceOverflow
case outsideDetailedView
case inexactCellConversion
case unrepresentableResult
}
func screenCoordinate(
point: AxisPosition,
origin: AxisPosition,
zoom: Double
) throws -> Double {
let cellSize = 1024.0
let screenOrigin = 512.0
guard point.local.isFinite,
origin.local.isFinite,
point.local >= 0, point.local < cellSize,
origin.local >= 0, origin.local < cellSize,
zoom.isFinite, zoom > 0 else {
throw CoordinateError.invalidInput
}
let difference = point.cell.subtractingReportingOverflow(origin.cell)
guard !difference.overflow else {
throw CoordinateError.cellDifferenceOverflow
}
guard (-1...1).contains(difference.partialValue) else {
throw CoordinateError.outsideDetailedView
}
guard let cells = Double(exactly: difference.partialValue) else {
throw CoordinateError.inexactCellConversion
}
let localDifference = point.local - origin.local
let delta = localDifference.addingProduct(cells, cellSize)
let scaled = delta * zoom
guard delta.isFinite, scaled.isFinite,
delta == 0 || scaled != 0 else {
throw CoordinateError.unrepresentableResult
}
let screen = screenOrigin.addingProduct(delta, zoom)
guard screen.isFinite,
delta == 0 || screen != screenOrigin else {
throw CoordinateError.unrepresentableResult
}
return screen
}
@main
struct SceneCoordinates {
static func main() throws {
let cell: Int64 = 1_099_511_627_776
let step = Double(sign: .plus, exponent: -30, significand: 1)
let zoom = Double(sign: .plus, exponent: 30, significand: 1)
let absoluteBase = Double(cell) * 1024.0
let collapsed = (absoluteBase + step) - absoluteBase
let screen = try screenCoordinate(
point: AxisPosition(cell: cell, local: step),
origin: AxisPosition(cell: cell, local: 0),
zoom: zoom
)
let tiny = Double(sign: .plus, exponent: -600, significand: 1)
let largeZoom = Double(sign: .plus, exponent: 600, significand: 1)
let tinyScreen = try screenCoordinate(
point: AxisPosition(cell: 0, local: tiny),
origin: AxisPosition(cell: 0, local: 0),
zoom: largeZoom
)
precondition(absoluteBase.ulp == 0.25)
precondition(collapsed == 0)
precondition(screen == 513)
precondition(tinyScreen == 513)
print("Absolute spacing:", absoluteBase.ulp)
print("Absolute displacement:", collapsed)
print("Split-coordinate screen:", screen)
print("Tiny-coordinate screen:", tinyScreen)
}
}
شغّل الحزمة من مجلدها:
swift run SceneCoordinates
النتائج الرقمية المتوقعة هي 0.25 و0.0 و513.0 و513.0، على الترتيب. هذه نتائج مشتقة من المدخلات الاصطناعية المقدمة. جرى التحقق من تباعد binary64 وانهيار الإزاحة والتحجيم بقوى العدد اثنين باستخدام Python في بيئة البحث؛ ولم يكن مترجم Swift متاحًا هناك، لذا فُحص برنامج Swift من دون تنفيذه.
تحافظ النتيجة المستقرة الأولى على 2^-30 منفصلة عن عنوان الخلية الضخم، ثم تضربها في 2^30 لإنتاج وحدة عرض واحدة. وتستخدم الثانية 2^-600 مع تكبير مقداره 2^600؛ وكلا المدخلين وناتج ضربهما قابل للتمثيل في هذا المثال. صغر المقدار وحده لا يعني فقدان الدقة.
يستخدم البرنامج a.addingProduct(b, c) للتعبير عن a + b × c. توثّق Apple هذه العملية المدمجة التي لا تتضمن تقريبًا وسيطًا. وهي تقلل التقريب داخل تلك العملية؛ أما الطرح المحلي وأي تقريب سابق للمدخلات فيظلان منفصلين. addingProduct(::)
كيف تختار ميزانية دقة مناسبة؟
ابدأ بأصغر مسافة يجب أن تظل مرئية عند مستوى التكبير الحالي. للحصول على هدف توضيحي مقداره ربع وحدة عرض، اقسم 0.25 على التكبير لتحصل على ميزانية خطأ في فضاء المشهد. ثم افحص التباعد عند الإحداثيات المحلية والقيم الوسيطة المستخدمة في الحساب.
على سبيل المثال، عند تكبير مقداره 2^30، تكون الميزانية التوضيحية 2^-32 وحدة مشهد. عند الإحداثي المحلي 512، يكون تباعد Double هو 2^-43، وهو أدق بكثير من تلك الميزانية. أما عند الإحداثي المطلق 2^50، فالتباعد هو 0.25، وهو خشن جدًا. تفسر هذه القيم المحسوبة سبب فائدة نقطة إسناد محلية قريبة لهذا العرض تحديدًا.
التباعد أداة تشخيصية، وليس حدًا كاملًا للخطأ. فقد تسهم التحويلات والطرح والتحويلات الهندسية والتحويل النهائي للرسم في الخطأ. اختبر إزاحات معروفة عبر المسار كاملًا، بما فيه العارض الفعلي، قبل اعتماد نطاق للتكبير.
| الملاحظة | المشكلة الرقمية المرجحة | الإجراء التالي |
|---|---|---|
| إضافة إزاحة لا تغيّر الإحداثي المخزّن | تباعد التخزين أكبر من الزيادة | احتفظ بالإزاحة منفصلة أو اختر نقطة إسناد أقرب |
| تعمل الإحداثيات النسبية حتى التحويل | دقة الوجهة غير كافية | حوّل بعد طرح الأصل والتكبير؛ وافحص الخطأ الناتج |
| تصبح القيم لانهائية | تتجاوز إحدى العمليات النطاق المحدود | غيّر ترتيب التقييم أو قيّد العرض المدعوم |
| تصبح إزاحة غير صفرية مساوية للصفر | محاها التجاوز نحو الصفر أو التقريب | أعد التحجيم في مرحلة أبكر أو غيّر التمثيل المحلي |
| يرفض هذا البرنامج خلية بعيدة | جرى تجاوز سياسة العرض الدقيق | اختر أصلًا قريبًا أو استخدم حسابًا منفصلًا للعرض العام التقريبي |
في العرض العام التقريبي، قد يُسمح منطقيًا بأن تُعرض بعض التفاصيل الصغيرة بحجم أقل بكثير من وحدة عرض. حدّد هذا السلوك صراحةً. ويقيّد المثال عمدًا الحساب الدقيق بالخلايا المجاورة؛ ويتطلب توسيع هذا النطاق ميزانية دقة جديدة، لا مجرد حذف الشرط الوقائي.
ماذا يحدث عند المقاييس الصغيرة جدًا وأثناء الرسم؟
تميّز Apple leastNormalMagnitude عن القيم دون الطبيعية الموجبة الأصغر. للدون الطبيعية دقة أقل من القيم الطبيعية، لذا فإن أصغر عدد موجب ليس مقياس عمل افتراضيًا جيدًا. افحص النتائج الوسيطة إلى جانب المدخلات عند ضرب إزاحات ضئيلة في عامل التكبير. leastNormalMagnitude
يرفض المثال إزاحة غير صفرية يؤدي تكبيرها إلى الصفر أو يجعلها غير قابلة للتمييز عن أصل الشاشة. لكنه لا يضمن أن كل نتيجة غير صفرية تقع ضمن هامش سماح بصري. أضف فحص قبول قائمًا على هامش سماح يناسب لوحتك، وتعامل مع الأخطاء المطروحة صراحةً بدلًا من إحلال إحداثي الأصل محلها.
في لوحة ثنائية الأبعاد، احتفظ بموضع منقسم لكل محور واحسب الإحداثيين النسبيين قبل الرسم. وللتوسيع إلى ثلاثة أبعاد، توفر Apple simd_double3، وهو اسم مستعار لـ SIMD3<Double>. تظل كل مركبة قيمة فاصلة عائمة بدقة 64 بت لها حد دقة خاص بها. simd_double3
احتفظ بحالة المشهد المنقسمة باعتبارها الموضع المعتمد. ولا تحوّل إلى النوع الذي تتطلبه واجهة الرسم إلا إحداثيات الرسم النهائية القريبة، وقِس خطأ التحويل مقارنة بميزانية فضاء العرض. إذا ظلت هذه القيم النهائية ضخمة، فتحقّق أولًا مما إذا كان الكائن ينتمي إلى إطار العرض الحالي.
تحتاج اللوحة المستقرة إلى نطاق مدعوم معلن: عناوين خلايا صحيحة، وإزاحات محلية محدودة، وفترة تكبير، وخطأ مقبول في فضاء العرض. احتفظ بالتفاصيل قبل تكوين قيم كبيرة، واحسب نسبةً إلى أصل قريب، وتحقق من إزاحات ممثلة عند حدود ذلك النطاق.
المصادر
- Double — وثائق مطوري Apple
- ulp — وثائق مطوري Apple
- subtractingReportingOverflow(_:) — وثائق مطوري Apple
- addingProduct(::) — وثائق مطوري Apple
- leastNormalMagnitude — وثائق مطوري Apple
- simd_double3 — وثائق مطوري Apple
- تثبيت Swift — Swift.org
- إنشاء أداة لسطر الأوامر — Swift.org
- ما ينبغي لكل عالم حاسوب معرفته عن حسابات الفاصلة العائمة — Oracle