رفتن به محتوا

سه‌بعدی و رندر نسل سوم

فاز ۵ پلن. صحنه در apps/web/src/components/Scene3D.tsx و components/scene/ (مسیرهای scene/… ِ پایین همین پوشه‌اند)؛ رندرکننده و خطِ لوله در lib/gpu/ (نسل هفتم)؛ رندر در lib/render/، components/RenderPanel.tsx و components/render/ (RenderBridge، RenderOptions، SavedRenders).

بوم: SceneCanvas، رندرکنندهٔ WebGPU و فهرستِ three

Section titled “بوم: SceneCanvas، رندرکنندهٔ WebGPU و فهرستِ three”

صحنه در scene/SceneCanvas.tsx رندر می‌شود، نه Canvas ِ R3F. رفتار همان است — اندازه از react-use-measure، رویدادِ اشاره‌گر روی قاب، معلق‌شدن و خطای صحنه به بیرونِ بوم، پلِ زمینهٔ React و StrictMode با its-fine — ولی Canvas ِ R3F کلِ فضای نامِ three را ثبت می‌کند (extend(THREE)) و هیچ کلاسی از بسته بیرون نمی‌ماند. SceneCanvas با createRoot ِ خودِ R3F فقط THREE_CATALOGUE (scene/three-catalogue.ts) را ثبت می‌کند.

  • رندرکننده (نسل هفتم، ۲.۲): WebGPURenderer ِ three، ساخته و راه‌افتاده در createRenderer (lib/gpu/backend.ts) پیش از آنکه r3f ببیندش — هیچ چیز پیش از init() رسم نمی‌کند. روی WebGPU هر جا مرورگر بدهد و روی شاخهٔ WebGL2 ِ همان رندرکننده هر جا ندهد؛ یک رندرکننده با دو شاخه، همان صحنه، همان جنس‌ها (کلاسیک؛ خودِ رندرکننده به گره برمی‌گرداندشان) و همان خطِ لوله. انتخابِ شاخه: ?renderer=webgl2 برای یک زبانه، «موتورِ نمایش» در منوی «نمایش» برای این مرورگر (state/scene-prefs.ts، کلیدِ darzsaz.renderer؛ رده در darzsaz.quality)، وگرنه خودکار. شاخه‌ای که واقعاً رسم می‌کند روی بوم data-backend است و رده‌اش data-tier.
  • یک three: three در Vite نامِ مستعارِ three/webgpu است (vite.config.ts)؛ رندرکنندهٔ WebGL (۸۶٫۸ کیلوبایت gzip) در بسته نیست و هر وارد کردنِ نام‌دارِ چیزی WebGL-تنها بیلد را می‌اندازد — همان که بشکهٔ drei را پیدا کرد: drei فقط تکه‌به‌تکه (@react-three/drei/web/Html). دوربین و کنترلش (SceneCamera، SceneControls در scene/SceneCamera.tsx) خودمان‌اند: دوربینِ پیش‌فرضِ ریشه — ShiftCamera ِ عکس (۴.۳) — و camera-controls روی دوربینی از آنِ خودش (۴.۱ و ۴.۳، پایین).
  • اندازه: تکهٔ Scene3D ۳۶۶٫۲ از سقفِ ۳۶۸ کیلوبایت (gzip)؛ در ۲.۸ خام: WebGPU و TSL ِ three ۶۵۲٫۶، هستهٔ three ۲۳۷٫۷، r3f ۱۴۳٫۹، افزونه‌های three (GTAO، TRAA، شکوفه، FXAA، Line2، RoomEnvironment) ۴۹٫۹، کدِ برنامه ≈ ۴۵؛ از ۴.۱ camera-controls ۱۲٫۳ به‌جای OrbitControls (deploy.md).
  • پایداری (۷.۱۰): sceneSupported() پیش از سوار شدن می‌پرسد — WebGPU، یا WebGL2 که شاخهٔ پشتیبان لازمش دارد؛ WebGL1 به‌تنهایی دیگر بس نیست. رندرکننده‌ای که راه نیفتد «نمای سه‌بعدی در دسترس نیست» می‌دهد، نه ردِ بی‌صاحب. از دست رفتنِ دستگاه در هر دو شاخه به renderer.onDeviceLost می‌رسد (پیش از init() نشانده می‌شود) و webglcontextlost ِ خودِ بوم هم از لحظهٔ سوار شدن گوش داده می‌شود — زمینه‌ای که پیش از گوش دادنِ شاخهٔ WebGL2 از دست برود بومِ سیاهِ بی‌پیام می‌گذاشت.

عنصرِ تازهٔ JSX (<torusKnotGeometry>) یا مؤلفهٔ تازهٔ drei که عنصری می‌سازد، باید کلاسش در فهرست باشد؛ وگرنه در مرورگر «R3F: X is not part of the THREE namespace» می‌آید. test/three-catalogue.test.ts برچسب‌ها را از درختِ نحوِ هر TSX و createElement ِ مؤلفه‌های drei ِ واردشده (بشکه یا تکه) می‌خواند و هم کمبود و هم کلاسِ اضافه را می‌گیرد.

خطِ لوله و رده‌ها (نسل هفتم، ۲.۳–۲.۵)

Section titled “خطِ لوله و رده‌ها (نسل هفتم، ۲.۳–۲.۵)”

lib/gpu/pipeline.ts نمای زنده را می‌کشد و scene/ScenePipeline.tsx آن را با useFrame ِ اولویتِ یک جای gl.render ِ r3f می‌نشاند:

پیش‌گذر (عمق، نرمال، حرکت) → GTAO → گذرِ صحنه (رنگ، تابش، نشان‌ها)
→ خطِ دور → زمینه → TRAA → Neutral (بالا، پایین)
گذرِ صحنه (رنگ، نشان‌ها) → خطِ دور → زمینه → Neutral → FXAA (حداقل)
  • انسدادِ محیطی (lib/gpu/passes.ts): GTAO ِ three از پیش‌گذر، با builtinAOContext فقط روی نورِ محیط و هرگز روی نورِ کلید — کنجی که نور به آن می‌تابد برای کنج بودن تیره‌تر نیست؛ برای همین پیش‌گذر لازم است: گذرِ صحنه باید انسداد را پیش از سایه‌زنی داشته باشد. نمای زنده حالا انسداد دارد؛ تا نسل ششم فقط رندر داشت.
  • رده‌ها (lib/gpu/tiers.ts): «بالا» نسبتِ پیکسلِ ۲، GTAO با نیم‌تفکیک و ۱۶ نمونه، TRAA، شکوفه؛ «پایین» نسبتِ ۱، GTAO نیم‌تفکیک با ۸ نمونه، TRAA، شکوفه؛ «حداقل» نسبتِ ۱، بی پیش‌گذر و انسداد، FXAA. WebGPU از «بالا» و WebGL2 از «پایین» شروع می‌کند. TRAA و نه SMAA (پلن SMAA گفته بود): یک گذرِ حل روی پیش‌گذری که انسداد به هر حال می‌خواهد، و نمای ساکن به تصویرِ ابرنمونه‌گرفته همگرا می‌شود؛ SMAA سه گذر و فقط فضایی است. GTAO ِ تمام‌تفکیک در نسبتِ ۲ بیشترِ قاب بود: «بالا» ۱۶٫۸–۲۱٫۱ میلی‌ثانیه در کارتِ Apple، با نیم‌تفکیک ۹٫۶–۹٫۸.
  • پالایش در سکون (lib/gpu/loop.ts): پس از آخرین فریمی که چیزِ دیگری خواست، حلقه خودش refine فریمِ دیگر می‌خواهد (۲۴ در «بالا» و «پایین»، صفر در «حداقل») تا TRAA و نوفهٔ چرخانِ انسداد بنشینند، و بعد هیچ.
  • رده‌گردان (Governor): زیرِ «خودکار» فاصلهٔ فریم‌های پالایش سنجیده می‌شود — فریمی که حلقه خودش خواسته با یک فریمِ نمایش پس از قبلی می‌آید اگر دستگاه برسد — و اگر میانهٔ سه فاصلهٔ پیاپی از ۳۳ میلی‌ثانیه گذشت، یک رده پایین؛ شش فاصلهٔ اولِ هر خطِ لوله (بارگذاری و کامپایل) و پس از دوازده فاصلهٔ خوبِ پیاپی دیگر سنجیده نمی‌شود. نخست تأخیرِ خواندنِ یک پیکسل سنجیده می‌شد: در WebGPU آن خواندن تا سرکشیِ بعدیِ مرورگر به دستگاه می‌ماند و کارتِ Apple که «بالا» را در ۹٫۷ میلی‌ثانیه می‌کشد بیش از ۳۳ خوانده شد و بی‌جهت «پایین» گرفت. انتخابِ دستی («کیفیتِ نمایش» در «نمایش») رده‌گردان را کنار می‌گذارد.
  • خطِ دورِ انتخاب (lib/gpu/outline.ts، ۲.۵): هر مش در هدفِ دیگری از همان گذرِ صحنه شناسهٔ کوچک و نوعِ نشانش را می‌نویسد و یک گذرِ تمام‌صفحه جای عوض شدنِ شناسه را کهربایی (MARK) می‌کند — درونِ شکل، پیش از ضدِّدندانه. یونیتِ انتخاب‌شده یک شکل است، قطعهٔ زیرِ اشاره‌گرِ فهرستِ قطعات شناسهٔ خودش را دارد، یونیتِ زیرِ اشاره‌گر کم‌رنگ‌تر. آنچه مش می‌نویسد از دو جاست: کلیدِ یونیت و قطعه‌اش، ثابت در عمرِ قطعه، یکنواختِ خودِ مش (userData ِ گروهِ قطعه، partKeys، onObjectUpdate)؛ و آنچه الان نشان دارد، یکنواختِ کلِ رندر (setMarks از RoomParts: برای هر شناسه کلیدِ یونیتی که با آن کشیده می‌شود و شدتش، و قطعهٔ فهرست). چرا دو جا: three یکنواختِ خودِ مش را فقط وقتی دوباره بار می‌کند که چیزی که می‌پاید عوض شود — جنس، هندسه، ماتریس — یا جنس یا بافتارِ گذر گره داشته باشد (NodeMaterialObserver.containsNode)، و انتخاب هیچ‌کدام نیست. نشان پیش‌تر یکنواختِ هر مش بود و فقط بافتارِ GTAO، که three را وامی‌دارد هر مش را هر فریم تازه کند، زنده‌اش نگه می‌داشت: «حداقل»، بی GTAO و ردهٔ پایانیِ «خودکار» برای دستگاهِ کند، هیچ خطی نمی‌کشید (۰ پیکسلِ نشان‌دار برای کابینتِ انتخاب‌شده در برابرِ ۵۰٬۸۳۵). بافتاری بی‌انسداد برای «حداقل» هم درستش می‌کرد و تصویر را پیکسل‌به‌پیکسل همان می‌گذاشت، ولی اسکریپتِ هر فریم را با پردازندهٔ چهار برابر کندتر ۴۲٪ بالا برد (۲۸٫۷ ← ۴۰٫۷ میلی‌ثانیه)؛ یکنواختِ رندر هر رندر ۴ کیلوبایت است (۳۰٫۵، در حدِ نوفهٔ سنجش). دو یونیتِ نشان‌دار با یک شناسه: یکی بی‌نشان می‌ماند، هرگز انتخاب به خاطرِ اشاره‌گر. e2e/outline.spec.ts خط را روی «حداقل» (WebGL2) و «خودکار» (WebGPU) می‌سنجد. OutlineNode ِ three نه: برای هر فریمِ با انتخاب صحنه را دو بارِ دیگر می‌کشید و یک انتخاب بیشتر نمی‌شناخت. تابشِ پیشین روی خودِ جنس بود و رنگِ ورقِ انتخاب‌شده را عوض می‌کرد؛ جنس‌ها حالا فقط به سطح، کم‌رنگی و شیشه/فلز بسته‌اند.
  • زمینه (lib/gpu/backdrop.ts): رنگِ رابط است نه نورِ اتاق و پیکسلش باید همان توکن باشد، ولی رندرکنندهٔ WebGPU هر چه گذرهایش بکشند نگاشت می‌کند. پس گرادیان به‌صورتِ نوری ذخیره می‌شود که با Neutral به همان توکن برمی‌گردد (unneutral ِ lib/gpu/neutral.ts، وارونِ بسته‌شکلِ منحنی؛ رفت‌وبرگشتِ هر توکن زیرِ ۰٫۰۲ سطح) و پیش از ضدِّدندانه زیرِ صحنه گذاشته می‌شود. پس از TRAA جایی نداشت: حلِ TRAA جنسِ مات است و آلفا را یک می‌کند. همه چیزِ گذرِ صحنه به صراحت شفاف پاک می‌شود (MRTNode.setClearColor): three هدفِ اولِ گذر را با رنگِ پاک‌کنِ رندرکننده و بقیه را سیاهِ مات پاک می‌کند، و جای خالی آلفای ۱ داشت.
  • هزینه (bench، آشپزخانهٔ نمونه/۴۰ یونیت، ۱۴۰۰×۹۰۰): کارتِ Apple با نسبتِ ۲ — رندرکنندهٔ قدیم ۴٫۰/۳٫۸ میلی‌ثانیه؛ «حداقل» ۳٫۵/۳٫۶، «پایین» ۴٫۶/۴٫۵، «بالا» ۹٫۷/۷٫۳ (WebGPU). SwiftShader با نسبتِ ۱ — قدیم ۲۹۰/۲۶۳؛ «حداقل» ۴۲۱/۳۷۴، «پایین» ۴۹۱/۷۲۴؛ خودِ رندرکنندهٔ تازه بی خطِ لوله ۳۴۶ برای ۴۰ یونیت (+۳۲٪): شیدرِ گره‌ای روی CPU گران‌تر است. رده‌گردان SwiftShader را در چند فریم به «حداقل» می‌برد (e2e/scene-idle.spec.ts).

حالتِ باز (نسل ششم، فاز ۳)

Section titled “حالتِ باز (نسل ششم، فاز ۳)”
  • حرکت در هندسه است، نه در وب: هر PartInstance ِ متحرک frontId (گروهِ چیزهایی که با هم می‌جنبند) و motion دارد — swing، slide، follow، fold — و poseFor(part, leaders, t) جای آن را در لحظهٔ t (۰ بسته … ۱ باز) می‌دهد. قراردادِ علامت در geometry/src/motion.ts نوشته است: چرخشِ مثبت حولِ axis لبهٔ آزاد را به سمتِ اتاق می‌برد، و محور از ضربِ برداری ساخته می‌شود تا علامتِ دستی نتواند دو بار غلط باشد (دربِ نسل پنجم درونِ بدنه باز می‌شد). خطِ لولا روی صفحهٔ جلوی بدنه و یک ضخامت بیرونِ لبهٔ لولادار است — کارِ بازوی لولای اروپایی، که بی آن دربِ لولای ۱۱۰ درجه گوشهٔ خودش را ۶ میلی‌متر درونِ پهلو می‌برد. آزمونِ پذیرش: geometry/test/open.test.ts (هر آشپزخانهٔ نمونه، هر نما، هر زاویه، بی برخورد).
  • چقدر باز در وب می‌ماند: openness: Map<frontId, 0..1> در Scene3D. «درها باز» همه را باز می‌کند، کلیک روی یک نما فقط همان را (و بارِ دوم می‌بندد؛ انتخاب دست نمی‌خورد)، و لغزندهٔ «میزان باز شدن درها» مقدارِ همه است و کلیک‌های تکی را پاک می‌کند.
  • بازشو در صحنه (فاز ۳.۴ و ۳.۵): دستگیره از ماشین‌کاری می‌آید، نه از حدس — handleSpot دو سوراخِ همان قطعه‌ای را می‌خواند که مته می‌خورد (handleHoles، و در دربِ قاب‌دار قیدِ آزاد یا صفحهٔ وسط) و میله را میانِ آن دو می‌گذارد؛ پس نقشه و قطعهٔ سوراخ‌شده نمی‌توانند اختلاف داشته باشند. دستگیرهٔ ریلی سوراخ ندارد و به پهنای نما روی لبه می‌نشیند. یونیتی که بازشوش دستگیره نیست هیچ میله‌ای ندارد؛ به‌جایش: پروفیل C/L آلومینیومی درونِ همان شکافی که نماها برایش کوتاه شده‌اند (channel-instances.ts، metal)، و شیارِ فرزخورده به‌صورتِ یک مستطیلِ تیره آنجا که دیده می‌شود — شیارِ انگشتی روی رویه، J روی خودِ لبه (gripMark). نمای دستگاه با دستگاهش باز می‌شود: ظرف‌شویی و فر به پایین، یخچال به پهلو، یک ربعِ دور.
  • اتاق و متعلقات (۳.۶ تا ۳.۹): یک ماده به ازای هر ظاهر و نه هر قطعه (src/scene/material-cache.ts: آشپزخانهٔ نمونه ۹۲ ← ۵)، سایه فقط از پوستهٔ بیرونی — دستگیره هم نه: در نقشهٔ سایهٔ نورِ اصلی (۲٫۲۵ میلی‌متر هر تکسل) سایهٔ میلهٔ ۱۳ میلی‌متری چند تکسل است و گشتاورهای نیم‌دقتِ VSM آن را ابرِ لکه‌لکه می‌کردند (انحرافِ روشنایی کنارِ میله ۳٫۲۴ ← ۱٫۰۶؛ درِ تمیز ۰٫۷۰)؛ تماسش را انسداد می‌کشد — پیش‌فرضِ دیوار گچِ گرمِ روشن (DEFAULT_FINISHES)، مانع با شکلِ نوعِ خودش و برچسبِ شناور در انتخاب (Obstacles3D)، متعلقاتِ دیوار — پهلو، تاج، قرنیز، پرکن — از همان قطعه‌ای که بریده می‌شود (accessory-instances.ts؛ برابری در سطحِ پروژه)، و «قاب» با درهای باز (openBoundsOf).
  • میان‌پرده در src/scene/openness.ts، خالص و بی three: ۳۲۰ میلی‌ثانیه، درب با easeOutCubic (لولای آرام‌بندِ کاتالوگ) و کشو خطی (ریل چیزی برای میرا کردن ندارد)، و وقتی همه با هم باز می‌شوند هر کدام ۳۰ میلی‌ثانیه پس از قبلی راه می‌افتد. prefers-reduced-motion همان فریم سرِ جا. یک useFrame در RoomParts همهٔ گروه‌ها را با applyPose می‌نشاند — بی رندرِ دوبارهٔ React.

حلقهٔ رسم، آزادسازی، از دست رفتنِ زمینه (فاز ۷)

Section titled “حلقهٔ رسم، آزادسازی، از دست رفتنِ زمینه (فاز ۷)”
  • frameloop="demand" (۷.۸، V18): صحنه فقط وقتی چیزی عوض شد رسم می‌شود. r3f با تغییرِ props خودش invalidate می‌کند و SceneControls با رویدادهای حرکتِ camera-controls (wake، control، update)؛ هر تغییرِ بیرون از props دستی: RenderBridge (پس از رندرِ آفلاین که اندازهٔ بوم را عوض و برگرداند)، باز شدنِ نماها (یک useFrame در RoomParts برای کلِ صحنه؛ تا نماها نرسیده‌اند فریم می‌خواهد؛ dt سقفِ ۱/۳۰ دارد چون فریمِ اولِ پس از سکون فاصله تا فریمِ قبلی را می‌گیرد)، و پالایشِ خطِ لوله (بالا). صحنهٔ ساکن صفر فریم: e2e/scene-idle.spec.ts فریم‌های خودِ خطِ لوله را می‌شمارد (window.__darzsazSceneFrames، هر دو شاخه یکی) — فراخوانِ رسمِ WebGL را شاخهٔ WebGPU اصلاً ندارد و خطِ لوله در هر فریم ده‌ها از آن می‌کند.
  • آزادسازی (V19): geometry={…} را r3f آزاد نمی‌کند. هندسهٔ مالِ جزء (صفحهٔ سوراخ‌دار، WallWash) در پاک‌سازیِ اثر dispose می‌شود؛ انبارهٔ panelGeometryCached شمارِ کاربر دارد (retainGeometry): آخرین کاربر که رفت، بافرِ کارت گرافیک پس از ریزکار همان‌جا آزاد می‌شود و هندسه از انباره هم می‌رود — قطعهٔ بعدیِ همان اندازه هندسهٔ تازه می‌گیرد. رندرکنندهٔ WebGPU dispose ِ هر هندسه را فقط یک بار، در بارگذاریِ اول، گوش می‌دهد؛ هندسهٔ آزادشده‌ای که دوباره داده می‌شد بارِ دومش هرگز آزاد نمی‌شد (۱۸ بافر پس از ۵۰ ویرایش، یک اجرا از دو). سرریز (۸۰۰) قدیمی‌ترین‌های بی‌کاربر را برمی‌دارد. e2e/scene-idle.spec.ts بافرهای زندهٔ WebGL را پیش و پس از ۵۰ ویرایشِ عرض می‌شمارد (۶۷۸ ← ۶۷۸)، و «آرام» را یک ثانیه بی تغییرِ شمار و بی فریم می‌گیرد: با فریمِ ۰٫۴ ثانیه‌ای دو خوانشِ ۸۰۰ میلی‌ثانیه‌ای در یک مکث می‌افتادند.
  • بودجهٔ کارایی (۷.۹، e2e/perf.spec.ts، تنها با --project perf --no-deps): ۴۰ یونیت، p50 و p95 ِ فاصلهٔ فریم در کشیدنِ مرزِ دوبعدی، کشیدن در «هر دو» و چرخاندنِ صحنه؛ آستانه ≈ ۱٫۵ برابرِ سنجیده روی SwiftShader. نشانگرِ روی یونیت در چرخاندن (دکمهٔ فشرده) عوض نمی‌شود — آشتیِ همهٔ قطعه‌ها در هر گذر چرخش را کند می‌کرد (p50 ۱۴۲۳ ← ۱۱۴۴، p95 ۳۴۵۴ ← ۲۲۵۷ میلی‌ثانیه).
  • انبارهٔ سطح با سقف (V20، lib/textures/index.ts): ۱۶ سطح (≈ ۶۷ مگابایت)، کم‌استفاده‌ترین آزاد می‌شود؛ سطحِ هنوز سوار را three از همان بومِ پخته دوباره بار می‌کند.
  • «هر دو» ارزان (۷.۸، V17): Scene3D پروژه را با useFrameBatched حداکثر یک بار در هر فریم می‌خواند (یک درخواستِ درراه، نه لغو با هر تغییر — وگرنه کشیدنِ پیوسته هرگز به فریم نمی‌رسید) و projectScene(project, cat, cache) را یک بار صدا می‌زند (Room صحنه را پارامتر می‌گیرد). انبارهٔ createSceneCache() ِ همان نما قطعه‌های هر یونیت را با هویتِ شیءِ یونیت نگه می‌دارد: immer یونیتِ دست‌نخورده را همان شیء می‌گذارد، پس فقط یونیتِ تغییرکرده PartInstance ِ تازه و هندسهٔ تازه می‌گیرد.
  • نورِ نوارِ زیرِ کابینت (V21): lightStrips(project, cat) روی دیوارِ خودش (wallTransform) و stripLightPoints(strips, 4) — همیشه چهار چراغ؛ بی کابینتِ دیواری خاموش (شدتِ صفر)، نه برداشته. از ممیزیِ نسل هفتم چراغ‌ها رو به پایین‌اند (SpotLight، مخروطِ ۵۷°، INTENSITY ۰٫۴۸ ← ۰٫۴ ِ نورِ واحد روی صفحهٔ کار): نقطه‌ای بودند و دیوارِ هم‌ترازشان، ۲۷ سانت دورتر، بیش از نورِ کاملِ یک نما می‌گرفت — نواری روشن درست زیرِ کابینت، جایی که نوارِ رو به پایین چیزی نمی‌فرستد (روشن در برابرِ خاموش: +۳۶ سطح در نمای زنده، +۲۳ در عکس). Since the plan-v7 audit: a strip runs only under units it can be fixed to (a bottom panel, not the hood’s unit — the hood has lamps of its own, and heat and grease under it), in a 17 × 7 aluminium profile flush with their underside, 50 mm behind the carcass’s front edge (LED_PROFILE); only its opal diffuser glows, facing down (UnderCabinetLights, components/scene/). It hung 10 mm below the units at mid-depth and ran on under the hood, a hot line of bloom. The wall catches a strip’s light in its own vertex colours (washAt → WallWash, STRIP_WASH), as a line of light facing down lights a wall a behind it: a·b/r³ — nothing level with the strip (the wall unit’s shade in the photo), most at b = a/√2, gone by the worktop; its peak 0.35, the worktop’s 0.4 under a strip times the ratio of the two — four lights on the wall had drawn four scallops, and three’s area light needs a 108 kB table the chunk has no room for. The photo drops the painted light and traces a rectangle light per diffuser (lib/photo/lights.ts). Those vertex colours carry nothing else: they darkened the wall away from the kitchen to 0.4 and tinted it 2% blue, and over the wall units the wall read 191 where the traced photo reads 241 (sRGB, the elevation); now 240.
  • هود (hoodFixtures, Hood in Fixtures.tsx): inside its unit, flush with the carcass’s front edge and 4 mm clear of the back panel’s face (hoodRoom); it stood on the wall, its back one plane with the back panel’s, and from outside the room the two fought in static. Drawn as a built-in hood is made: a dark body, a brushed-steel visor across its front, two grease filters and two lamps underneath.
  • گاز، سینک و شیر (ممیزیِ نسل هفتم؛ FixturesHob.tsx، FixturesSink.tsx): گاز شیشهٔ سیاه است با سرشعله و کلاهکِ لعابی روی هر شعله، شبکهٔ چدنی رویش (حلقه روی چهار پایه و چهار انگشت) و پنج پیچِ جلو میانِ دو شعلهٔ جلو — پیش‌تر تخته‌ای سیاه با پنج حلقهٔ تخت بود. شبکه و پیچ سایه نمی‌اندازند (مثلِ دستگیره). شیر یک مخلوطِ اهرمی است: پایه، بدنه با اهرمِ کنار، و لوله‌ای که بالا می‌رود و در نیم‌دایره‌ای رو به لگن برمی‌گردد، دهانه‌اش ۱۸۰ میلی‌متر جلوتر از شیر — لولهٔ ۱۲۰ میلی‌متریِ پیشین رو به دیوار بود (z ِ منفی)؛ آزمون دهانهٔ شیرِ هر آشپزخانهٔ نمونه، دیوارِ چرخیده هم، را روی لگنِ خودش می‌خواهد. خروجیِ سینک فلنج و صافی و دکمه دارد.
  • بازتابِ فلز (ReflectionProbe، Metal، lib/gpu/reflection.ts، scene/probes.ts): فلز نورِ پخشی ندارد و فقط آینه می‌کند؛ با محیطِ کالیبره (۰٫۲۹۵) شیرِ کروم اتاقی کم‌نور و ساختگی را آینه می‌کرد و سیاه بود — روی نمای روبه‌رو ۱۰۹ در برابرِ ۱۴۸ ِ عکسِ ردیابی‌شده. دو کاوشگر خودِ اتاق را، با همین نورها و به همان واحدِ تصویر، در مکعبی ۱۲۸ تکسلی می‌گیرند و برای هر زبری پیش‌پالایش می‌کنند (Probe): روی صفحه، یک دست بالای سینک، برای آنچه روی صفحه است (شیر، لگن، خروجی، گاز، پروفیلِ نوار)، و در اتاق، میانِ کفِ اتاق به بلندیِ دستگیره (۱٫۱ متر)، برای آنچه روی نماست (دستگیره، دستگاه، هود، آلومینیومِ material-cache). کاوشگر فقط جای خودش را می‌شناسد: با یک کاوشگرِ میانِ صفحه، نگاهِ پایینِ دستگیره سنگِ صفحه بود و دستگیره سیاه (۵۱ از ۲۵۵ از بالا)، و دیوارهٔ لگن هم (۳۸)؛ حالا دستگیره کفِ اتاق را آینه می‌کند (۱۳۴) و دیوارهٔ لگن لگن را (۸۸). با یک کاوشگرِ میانِ اتاق دیوارهٔ پشتِ سینک و پیچ‌ها قهوه‌ای می‌شدند. برنامه به ازای کاوشگر (PROBE_KEY → customProgramCacheKey): three برنامه را با نگاشت و نمونه‌گیرِ بافت کلید می‌کند نه با خودِ بافت، و EnvironmentNode بافتِ پیش‌پالودهٔ نخستین ماده را در برنامه می‌پزد؛ دو فلزِ یکسان جز در کاوشگر یک برنامه داشتند و لگن کفِ چوبیِ کاوشگرِ اتاق را آینه می‌کرد با این‌که ماده‌اش کاوشگرِ صفحه را داشت. پس از هر تغییر و ۳۰۰ میلی‌ثانیه سکون دوباره، هر کاوشگر در کارِ جدا؛ هزینه در Chrome روی صفحه ۱۴ میلی‌ثانیه نخِ اصلی و ۴۰ در کل، در اتاق ۵ و ۵۵؛ اولی خط‌لولهٔ همهٔ مواد را برای هدفِ کاوشگر می‌سازد: ۵۲۵ (یک کاوشگرِ تنها ۴۳۸). بافتی که فلزها می‌خوانند یکی است و عوض نمی‌شود، پس هر ماده فقط یک بار (از محیط به کاوشگر) از نو ساخته می‌شود: گرفتنِ اول در آن است و هر گرفتنِ بعدی در هدفِ دوم، که در آن کپی می‌شود (takeProbe، copyTextureToTexture). پیش‌تر هر گرفتن در همان بافت بود: گذری که بافتی را می‌نوشت که فلزهای درونِ اتاق در همان گذر از آن می‌خواندند، و WebGPU چنین گذری را رد می‌کند: در آشپزخانهٔ نمونه هر تغییرِ «نمایش» ۱۶ تا ۱۸ خطای اعتبارسنجی بود و بازتاب همان قبلی می‌ماند (۵.۳). بهای هدفِ دوم، از اولین گرفتنِ دوباره: بافت ۶۳ ← ۶۷. ماتیِ دستگاه در برابرِ عکس (۰٫۵۰ و ۰٫۷۰ روشناییِ خطی) می‌ماند: عکس جعبه‌نورهای بزرگی دارد که فلز آینه‌شان می‌کند و نورهای جهت‌دارِ نمای زنده دیده نمی‌شوند، و دیوارِ روبه‌روی آشپزخانه در نمای زنده جز محیط نوری ندارد.
  • کارتِ قطعه (۴.۶، scene/PartCard.tsx): نشانگر که ۳۵۰ میلی‌ثانیه روی قطعه‌ای بماند، کارتی کنارش می‌آید با نامِ قطعه و یونیتش، اندازهٔ نهایی (پس از نوار)، جنسِ ورق و چهار لبه — نمای کوچکِ همان قراردادِ لیست برش (طول افقی، لبه‌های طول بالا و پایین، نوارخورده پررنگ، فارسی‌بر به رنگِ خطر) و نامِ نوارها. داده همان Part ِ لیست برش است که به برشکار می‌رسد (lib/part-card.ts ← cardOf): شناسهٔ قطعهٔ صحنه و هسته برای بدنه یکی است (<unit>:side:left) و برای نما نه (صحنه ردیف و خانه، هسته شمارهٔ نما)، پس جایی که شناسه می‌گوید شناسه، وگرنه همان نقش و همان سه اندازه، به همان ترتیب — همان سنجه‌ای که برابریِ هسته و صحنه رویش است؛ آزمون همهٔ قطعه‌های بریدنیِ همهٔ آشپزخانه‌های نمونه را جفت می‌کند. صفحهٔ کار و آنچه خریدنی است (پاخورِ PVC، بدنهٔ فلزیِ کشو) نام و جعبهٔ خودشان را می‌گویند. نشانگر در فروشگاهِ کوچکِ خودش است (lib/pointed-part.ts) تا هر حرکت فقط کارت را بکشد، نه همهٔ قطعه‌ها؛ در اندازه‌گیری و چرخاندن کارت نیست. تصویری از همان که فهرستِ قطعه‌های بازرس با کلمه می‌گوید، پس بیرون از درختِ دسترس‌پذیری (aria-hidden).
  • انتخاب در سه‌بعدی (۷.۱۱، V24): همهٔ selection (نه فقط انتخابِ اصلی) با خطِ دورِ MARK (بالا، «خطِ دورِ انتخاب»)؛ یونیتِ زیرِ نشانگر کم‌رنگ‌تر (pointed، وضعیتِ همان Room)؛ کلیک روی جای خالی (onPointerMissed ِ r3f، تا ۲ پیکسل جابه‌جایی) انتخاب را برمی‌دارد، جز در اندازه‌گیری. F (scene.focus در رجیستری، لایهٔ scene) با تمرکز درونِ صحنه — قابِ بوم tabIndex={-1} است تا کلیک تمرکز را بیاورد — focusPose محدودهٔ انتخاب را از همان سوی دوربین در قاب می‌گذارد.
  • پایداری (۷.۱۰، V22): SceneCanvas در سوار شدن sceneSupported() را می‌پرسد (بالا) و با از دست رفتنِ دستگاه جای بوم SceneStopped می‌گذارد: «نمای سه‌بعدی متوقف شد» با «دوباره» (بوم و رندرکنندهٔ تازه با key) و «برو به دوبعدی»؛ بی WebGPU و WebGL2 «نمای سه‌بعدی در دسترس نیست» بی «دوباره»؛ تکهٔ تنبلِ صحنه که نرسد (SceneChunk) «نمای سه‌بعدی بار نشد» با «دوباره» (lazy ِ تازه).

scene/presets.ts: شش نمای آماده از محدودهٔ صحنه و نسبت پنل — سه‌رخ (همان قاب‌گیری پیش‌فرض)، روبه‌رو، بالا، گوشهٔ چپ/راست، «نمای کار» (چشم ۱۶۰ سانت، ۲۲۰ سانت جلوی جلوترین لبهٔ صحنه). «ذخیرهٔ نمای فعلی» → project.views[] = { id, name, pos, target } (متر، مختصات صحنه) و در همان منو فهرست می‌شود؛ target نقطه‌ای روی خطِ دیدِ دوربین است به فاصلهٔ نقطهٔ گردش (poseOf)، نه خودِ نقطهٔ گردش — پس از گردش دورِ نقطهٔ زیرِ نشانگر دوربین از کنارِ آن نگاه می‌کند. pos جای خودِ دوربین است، نه getPosition ِ camera-controls که جابه‌جاییِ کانونیِ آن گردش را ندارد: نمایی که پس از گردش دورِ نقطه‌ای بیرون از وسط ذخیره می‌شد از جایی برمی‌گشت که کسی آنجا نایستاده بود (آزمون: (۰٫۸، ۱٫۴، ۵٫۰۷) به‌جای (۰، ۱، ۵)). نما لنز ندارد: جای ایستادن و نگاه است، و لنز ترجیحِ دستگاه (پایین).

کنترلِ دوربین (نسل هفتم، ۴.۱؛ scene/view-controls.ts، SceneControls، scene/use-view.ts): camera-controls (MIT، یک پرونده، بی‌بستگی به رندرکننده) به‌جای OrbitControls ِ three و نزدیک/دورِ دست‌سازِ پیشین. رفتن به نما — آماده، ذخیره‌شده، «F»، «قاب» — نرم است (smoothTime ۰٫۲۵: در عکس‌های ۶۰، ۱۵۰، ۲۵۰ و ۴۰۰ میلی‌ثانیه پس از کلیک، حرکت تا ۲۵۰ به جا رسیده) و با «کاهشِ حرکت» ِ دستگاه در دم؛ قابِ تازه‌ای که اندازهٔ تازهٔ پنل ساخته نرم نیست تا پشتِ دستِ کشندهٔ جداکننده نماند. گردش دورِ نقطهٔ زیرِ نشانگر: با شروعِ کشیدن با دکمهٔ چپ، پرتو از نشانگر به صحنه و setOrbitPoint روی اولین مش — نما نمی‌پرد، کتابخانه نگاه را با جابه‌جاییِ کانونی نگه می‌دارد؛ سنجیده: کشیدن از روی گاز، گاز زیرِ نشانگر ماند. نزدیک‌شدن به سوی نشانگر (dollyToCursor)، حدِ فاصله و زاویه مثلِ پیش، و صفحه‌کلید با تمرکز درونِ صحنه (scene/scene-keys.ts، از رجیستری): جهت‌نماها نما را دورِ نقطهٔ گردش می‌چرخانند (۱۵° دورتادور، ۱۰° بالا و پایین)، «+» (یا «=») و «−» نزدیک و دور — کسی که موس ندارد پیش‌تر نما را نمی‌توانست بچرخاند. چرخِ موس «شروعِ کنترل» ِ camera-controls را نمی‌دهد، پس خودش «دست خورد» ِ قاب را خبر می‌دهد.

قاب بیرون از زیرِ نوارها (نسل هفتم، ۴.۲): نوارهای شناور روی صحنه — نوارِ بالا (SceneBar) و ابزارها (SceneTools) — data-scene-bar دارند (OverlayBar ِ کیت صفت‌های data-* را به ریشه‌اش می‌دهد). usePanelSize اندازهٔ پنل را با مستطیلِ آن‌ها می‌سنجد و insetsOf (scene/bars.ts) آنچه هر کدام از لبهٔ صحنه می‌گیرد؛ هر نوار با ResizeObserver و آمدن و رفتنشان (تاشدنِ پنل) با MutationObserver. frameFor، نماهای آماده و «تمرکز» فاصله را برای بخشِ آزادِ پنل می‌گیرند (freePart) و دوربین و هدف را با هم کنار می‌برند تا وسطِ آشپزخانه وسطِ همان بخش باشد (intoFree)؛ دوربینی که کسی نچرخانده با تغییرِ نوارها دوباره قاب می‌شود. سنجیده با عکسِ صفحه (اتاق خاموش، نوارها پنهان، پیکسل‌های آشپزخانه زیرِ مستطیلِ هر نوار): ۱۴۰۰×۷۰۰ ۱٫۶٪ ِ آشپزخانه زیرِ نوارِ بالا و ۴٫۲٪ زیرِ پایینی، ۱۲۸۰×۶۲۰ ۲٫۷٪ و ۶٫۲٪ — حالا در هر سه اندازه ۰٪.

لنز و «عمودها صاف» در نمای زنده (نسل هفتم، ۴.۳؛ scene/ViewsMenu.tsx، scene/level.ts): منوی «نماها» گروهِ «لنز» (۲۴، ۳۵، ۵۰ با همان نام‌های عکس، lib/render/lens-names.ts) و «عمودها صاف» دارد؛ هر دو ترجیحِ دستگاه‌اند (useScenePrefs، darzsaz.lens و darzsaz.level)، پیش‌فرض ۳۵ (۳۷٫۸۵°، همان ۳۸° ِ پیشین) و خاموش — نمای کار برای چرخیدن است و سه‌نقطه‌ای، مثلِ SketchUp. پنلِ عکس لنزِ خودش را دارد (۳۵ و تراز، ۳.۵): لنزِ نگاه‌کردن و لنزِ عکس‌گرفتن دو تصمیم‌اند. لنز در همهٔ قاب‌گیری‌ها هست (FrameView.fov: frameFor، نماهای آماده، «تمرکز»)؛ دوربینی که کسی نچرخانده با لنزِ تازه دوباره قاب می‌شود (روی ۲۴ نزدیک‌تر، روی ۵۰ دورتر، آشپزخانه همان‌قدر)، و دوربینِ چرخانده‌شده سرِ جایش می‌ماند و بازتر یا تنگ‌تر می‌بیند (useFitFrame، همان قاعدهٔ اندازهٔ پنل). دوربینِ پیش‌فرضِ ریشه ShiftCamera ِ عکس است و camera-controls دوربینِ خودش را می‌چرخاند؛ دوربینِ کشیده‌شده هر فریم پیرو آن است: همان، یا تراز (levelCamera) — شیب دورِ محورِ افقیِ خودِ دوربین برداشته و قاب چنان لغزانده که خطِ دید وسطِ صفحه بماند. تا بُردِ لنزِ جابه‌جاشونده (MAX_SHIFT ۰٫۶ قاب: ۳۱° روی ۲۴، ۲۲٫۴° روی ۳۵، ۱۶٫۱° روی ۵۰) همهٔ شیب؛ از آن بیشتر کم‌کم کمتر، تا نگاهِ راست از بالا که هیچ — «بالا» نقشه است؛ نگه‌داشتنِ همهٔ بُرد آن را نمایی از ۲۲° پایین‌تر می‌کرد که رویش لغزیده باشد. در مرزِ بُرد پرشی نیست؛ عکس (levelFrame) آنجا به دوربینِ کج برمی‌گردد، که در نمای زنده زیرِ دستِ چرخاننده می‌پرید. سنجیده روی آشپزخانهٔ ال از ۲۰° بالا: لبهٔ ماشین‌ظرف‌شویی ۲٫۸۶° ← ۰٫۰۰° و پهلوی کابینتِ چپ ۶٫۱۸° ← ۰٫۰۰°؛ فشردن روی گاز با «عمودها صاف» (نقطهٔ گردش) تصویر را عوض نکرد (میانگینِ اختلاف ۰٫۱ از ۲۵۵). چرا دوربینِ جدا: camera-controls دوربینش را می‌خواند (جابه‌جاییِ کانونیِ setOrbitPoint و کشیدنِ جانبی در محورهای آن)، و نمای ذخیره‌شده هم؛ روی دوربینِ تراز هر کدام تراز درمی‌آمد و گردشی که با «عمودها صاف» شروع می‌شد می‌پرید (آزمونِ scene-camera.dom). آنچه زیرِ نشانگر است از دوربینِ کشیده‌شده پرسیده می‌شود، و عکسِ «نمای فعلی» خطِ دیدِ وسطِ قاب را می‌گیرد (lineOfSight)، نه محورِ دوربینِ تراز را.

  • دستگاهِ یونیت: z از دیوار به بیرون. یونیت با پشت‌بندش به دیوار تکیه می‌دهد: پشت‌بندِ روکار در [0, ضخامت] و بدنه از پشتِ آن (depthFromWall = totalDepth)؛ پیش‌تر پشت‌بند درونِ دیوار (z منفی) بود. نمای دستگاهِ توکار هم از همان جلوی بدنه. صفحهٔ کابینت از خودِ دیوار اندازه می‌خورد (ورقِ بازار)، پس پیش‌آمدگی‌اش جلوی یونیتِ پشت‌بنددار به اندازهٔ پشت‌بند کمتر است — همان که نصب می‌شود.
  • چرخش: size ِ هر جعبه در دستگاهِ خودش است (x در امتدادِ دیوار) و rotY آن را می‌چرخاند؛ boundsOf و isoBox هر دو چرخش را می‌بینند. مرزِ صحنهٔ ال‌شکل همان مرزِ گوشه‌های چرخیده است (آزمونِ walls.test.ts).
  • کلاف‌ها به ترتیبِ هسته‌اند: rail:0 جلو، rail:1 عقب.
  • نمای روبه‌رو از دیوارِ جاری است (RenderBridge ِ wallId): دوربینِ متعامد سرِ گوشهٔ پایین-چپِ دیوار و ۱۲ متر جلوتر در امتدادِ نرمالش (lib/render/elevation.ts)، نورِ اصلی هم از جلوی همان دیوار (ELEVATION_KEY) و با همان سهمِ کالیبره‌اش (پایین، «نور و رنگ»). ارتفاعِ خروجی از پهنای مهارشده است (outputSize).

لبه و سوراخ روی جعبهٔ قطعه (نسل هفتم، ۵.۱)

Section titled “لبه و سوراخ روی جعبهٔ قطعه (نسل هفتم، ۵.۱)”

دیدِ کارگاهی (فاز ۵) از خودِ Part ِ لیستِ برش کشیده می‌شود، نه از حدسِ صحنه: هر جعبه‌ای که قطعهٔ بریدنی است cutId دارد — شناسهٔ همان قطعه در generateParts — و edges و holes ِ آن قطعه روی رویه‌های جعبه (packages/geometry/src/marks.ts).

  • چه کسی می‌گوید قطعه چطور ایستاده: همان تابعی که جعبه را می‌گذارد، با cut: { id, frame } در PartDraft. Emitter نگهش می‌دارد و unitInstances پس از ساختنِ قطعه‌های یونیت نشانه‌ها را می‌گذارد (withMarks)، با نوارِ سرِ ردیف از runExposure — پس کلیدِ انبارهٔ صحنه دیده‌شدنِ دو پهلو را هم دارد. PartFrame چهار سو در دستگاهِ جعبه است: x و y ِ ماشین‌کاری (x در طولِ قطعه از سری که از آن اندازه می‌خورد، y در عرض از لبه‌ای که از آن)، face رویه‌ای که ماشین‌کاری رویش اندازه می‌خورد (drilledFace: درونِ بدنه، پشتِ نما) به‌صورتِ نرمالِ بیرونیِ آن، و front سویی که «جلو» ی قطعه رو به آن است — اتاق، جز برای بازوی دومِ کابینتِ ال.
  • قراردادها، همه از هسته: پهلو از پا بالا و از لبهٔ جلو عقب؛ قطعهٔ افقی از سرِ چپ و از لبهٔ جلو عقب — کف از بالا، سقف و کلافِ خوابیده از زیر؛ درِ لولاپهلو از پا بالا و از لبهٔ لولا (frontFrame)؛ نمای خوابیده (کشو، بالابر) از سرِ چپ و از لبهٔ بالا پایین (parts/grip.ts)؛ پایهٔ قابِ کلاسیک مثلِ درش؛ کابینتِ ال برای کنجِ تهِ بازوی A، و آینه برای کنجِ سرِ آن. طبقهٔ ثابت هر دو رویه‌اش درونِ بدنه است و لیستِ برش رویهٔ قفلِ مینی‌فیکس و جای پیچِ پنهانش را به نصاب می‌سپارد؛ دیدِ کارگاهی آن‌ها را جایی می‌گذارد که دیده نشوند: زیرِ طبقه‌ای پایین‌تر از چشم، رویِ طبقه‌ای بالاتر از آن (کابینتِ دیواری). چشم ۱۶۰۰ میلی‌متر است، ارتفاعِ نمای هم‌ترازِ چشم (lookedUpAt).
  • لبه‌ها: چهار EdgeMark به ترتیبِ Part.edging (alongLength بعد alongWidth)، هر کدام روی رویه‌ای که نامش می‌گوید (front، top، … با frame.front)، با bandId ِ نوار یا null برای لبهٔ خام. رنگ و ضخامت از کاتالوگ (۵.۲).
  • سوراخ‌ها: هر MachiningOp ِ گرد یک HoleMark است — دهانه از مرکزِ جعبه، سو، قطر، عمق. سوراخِ رویه روی frame.face؛ سوراخِ لبه (face: 'edge') در سرِ x = 0 یا طول، وسطِ ضخامت و در امتدادِ قطعه. قطر و عمقی که هسته به کاتالوگ می‌سپارد از joinery.confirmat (رویه سرتاسری، لبه راهنمای پیچ)، و پیچِ صفحهٔ لولا و ریل Ø۳ مثلِ برگهٔ سوراخ‌کاری. شیار، جای دستگیرهٔ فرزی و بریدگی خط و مستطیل‌اند و اینجا نیستند.
  • آنچه برش ندارد cutId ندارد: صفحهٔ کابینت، قرنیزِ PVC، بدنهٔ فلزیِ کشو، درِ شیشه‌ای. تاج، زه و پرکنِ سقف در صفحهٔ جلو ایستاده‌اند و لیستِ برش از ۵.۳ لبه‌هایشان را همان‌طور نام می‌برد — بالا، پایین و دو سر؛ پیش‌تر به نامِ تخته‌ای خوابیده («جلو» با نوار، «پشت» به دیوار) و نشانه نداشتند. cutPartOf ِ کارتِ قطعه (۴.۶) از cutId می‌خواند.
  • هزینه: projectScene ِ سرد در Node روی همین مک: آشپزخانهٔ نمونه ۰٫۷ ← ۱٫۹ ms، ۴۰ یونیت ۱٫۶ ← ۳٫۵؛ انباره فقط یونیتِ تغییرکرده را دوباره می‌سازد.
  • پذیرش (geometry/test/marks.test.ts): ۱۵ پروژه (دو نمونه، الگوها، گالری)، ۱۲۹ یونیت و گونه‌هایشان — قرنیزِ MDF، نمای کلاسیک، تیغه با طبقهٔ متحرک، کشوی داخلی، طبقهٔ ثابتِ دیواری با مینی‌فیکس، سه اتصالِ دیگر — ۸۲۴ یونیت و ۹٬۴۵۶ قطعه در ۱۶ نقش. هر قطعه یک جعبه به اندازهٔ خودش؛ ۳۷٬۸۲۴ لبه (۲۵٬۴۵۲ خام) روی چهار رویهٔ دورِ ضخامت با نوارِ لیستِ برش؛ ۶۲٬۶۲۰ سوراخ درونِ جعبه، دهانه روی رویه‌اش. و آنچه جفت است: کاسهٔ لولا هم‌تراز و کنارِ صفحهٔ لولای پهلویی که در از آن آویزان است؛ هر اتصالِ رویه با سوراخِ سرِ قطعه‌ای که نگه می‌دارد در یک نقطه و به همان شمار؛ ردیفِ سیستم ۳۲ ِ دو پهلو یکی؛ دو سوراخِ دستگیره زیرِ میله؛ هر سوراخِ کور از رویهٔ رو به میانهٔ یونیت.
  • آنچه پیدا کرد: طبقه‌های طاقچهٔ دستگاه در ساختِ دوباره دو برابر (3a98b371)؛ طبقهٔ ثابت یک میلی‌متر دور از هر بدنه و پهلوهای کنجِ ال با سوراخِ کلافِ عقبی که ندارد (a41e871b)؛ کشوی بی‌ریل بی ریل در خرید، بی سوراخ و بی جعبه (a9aa7f22)؛ و در صحنه: پایه‌های درِ کلاسیکِ لولاراست جابه‌جا (دستگیره کنارِ لولاها)، میلهٔ نمای کشوی قاب‌دار ایستاده، میلهٔ نمای خوابیده نیم میلی‌متر دور از سوراخ‌هایش، کفِ کشوی داخلی ۳ میلی‌متر به‌جای ورقِ خودش.

نوار و مغزِ ورق روی لبه (نسل هفتم، ۵.۲)

Section titled “نوار و مغزِ ورق روی لبه (نسل هفتم، ۵.۲)”

لبهٔ هر ورقی که لیستِ برش می‌بُرد همان را نشان می‌دهد که آن لیست می‌گوید: لبهٔ نواردار به رنگِ نوارش (color ِ کاتالوگ)، لبهٔ خام به رنگِ مغزِ ورق — همان رنگی که درپوشِ برش از آن ورق نشان می‌دهد (CORE ِ scene/materials.ts: MDF، نئوپان، تخته‌چندلا، فیبر) — و ممبران، که فویلش دورِ لبه پرس شده، لبهٔ خامش هم رویه است (coreOf).

  • رنگ با هندسه است، نه با جنس: panelMesh برای هر رأس می‌گوید جزوِ کدام رویهٔ جعبه است (face، به ترتیبِ BOX_FACES): پخ جزوِ لبه‌ای است که گرد می‌کند و کنج جزوِ لبه‌ای که لیستِ برش اول نام می‌برد — لبهٔ بلند، که آخر نوار می‌خورد و سرِ نوارِ لبهٔ کوتاه را می‌پوشاند. panelGeometry از آن و از faceLooks (لبه‌های PartInstance.edges) برای هر رأس edgeLook (رویه ۰، نوار ۱، مغز ۲) و color (رنگِ نوار یا مغز، RGBA) می‌سازد و مثلث‌ها را به همین ترتیب می‌چیند، هر نگاه یک گروه. کلیدِ انبارهٔ هندسه نگاهِ لبه‌ها را هم دارد.
  • یک جنس برای همه: BoardMaterial (scene/board-material.ts) نگاه را از رأس می‌خواند: رنگِ نوار و مغز از color، مغز زبر (۰٫۸۵) و بی پوشش، نوار و مغز بی برجستگیِ رویه. گره‌ها یک بار ساخته می‌شوند و همهٔ ورق‌ها همان‌ها را دارند: three برنامه را با شناسهٔ خودِ گره‌ها کلید می‌زند، و گرهِ ساخته‌شده برای هر جنس برای هر جنس برنامه‌ای جدا می‌ساخت. ناظرِ جنس (setupObserver) گره‌ها را نمی‌بیند: three جنسی را که گره دارد در هر قاب برای هر مش دوباره به‌روز می‌کند، و این گره‌ها فقط هندسه و مقدارهای خودِ جنس را می‌خوانند.
  • هزینه، آشپزخانهٔ نمونه، «بالا»: فراخوانِ رسم ۵۵۲ = ۵۵۲، مثلث ۱۶۰٬۹۶۴ = ۱۶۰٬۹۶۴، هندسه ۱۶۲ ← ۱۶۷، برنامه ۱۱۵ ← ۱۲۲ (ورق و صفحهٔ کابینت حالا دو خانواده‌اند). زمانِ قاب روی همین مک از اجرا به اجرا ۱۴ تا ۵۲ میلی‌ثانیه می‌پرد و فرقی را نشان نمی‌دهد.
  • در «عکس»: ردیاب گره نمی‌خواند؛ هر ورق را سه جنسِ ساده می‌گیرد (BoardMaterial.traced)، نوار و مغز به رنگِ رأس‌هایشان. هر گروه یک مشِ جدا (standIns ِ lib/photo/scene.ts): three-gpu-pathtracer ‏0.0.24 صحنه را با یک گروه برای هر مش ادغام می‌کند و جنس‌های یک مش را پشتِ هم می‌شمارد، پس مشِ سه‌جنسی جنسِ همهٔ مش‌های بعدش را جابه‌جا می‌کرد (نماها قهوه‌ای، قرمزِ مانع، سیاهِ گاز). رنگ RGBA است چون ردیاب به هندسهٔ بی‌رنگ رنگِ چهارتایی می‌دهد و شاخهٔ سه‌به‌چهارش وارونه کپی می‌کند.
  • امتحان شد و نشد: نقشِ نئوپان و لایه‌های تخته‌چندلا — یک لایه ۱٫۴ میلی‌متر است، در نزدیک‌ترین دوربین (۰٫۶ متر) زیرِ دو پیکسل، پس نقش هر جا دیدنی بود باید محو می‌شد، و ردیاب تخت می‌کشیدش: دو تصویر از یک لبه. رنگِ مغز به‌صورتِ مرجعی به خودِ جنس (materialReference) هم: در گذری جنسِ دیگری می‌خواندش و صحنه از کار افتاد.
  • آزمون: geometry/test/mesh.test.ts (رویهٔ هر مثلث)، web/test/boxgeo.test.ts (گروه‌ها، رنگِ رأس، ممبران، انباره)، material-cache.test.ts (ناظرِ بی‌گره، سه جنسِ ردیاب)، photo-materials.test.ts (یک مش برای هر نگاه).

نمای نوار لبه (نسل هفتم، ۵.۳)

Section titled “نمای نوار لبه (نسل هفتم، ۵.۳)”

«نمای نوار لبه» در منوی «نمایش» است (DisplayOptions.edgeBands، پیش‌فرض خاموش؛ پنجرهٔ دوم هم از workspace-channel می‌گیردش). روشن که باشد رویه‌ها خاکستری‌اند و فقط لبه‌ها رنگ دارند: نوار به رنگِ خودش، لبهٔ خامی که دیده نمی‌شود به رنگِ مغزِ ورق، و لبهٔ خامی که اتاق می‌بیند نارنجی (MEDIA.rawEdge، --media-raw-edge).

  • کدام لبه نارنجی است از هسته می‌آید، نه از صحنه: rawEdgesInSight (core/src/parts/in-sight.ts) — همان فهرستی که قاعدهٔ raw-edge-seen ِ دستیار از آن هشدار می‌سازد، پس شمارِ نارنجی‌ها همان شمارِ لبه‌های هشدار است. کجا رو به کجاست از نام و نقشِ قطعه (sightsOf)؛ geometry/test/sight.test.ts روی جعبه‌های هر دیوار می‌سنجد که لبه‌ای که هسته دیدنی می‌داند به هیچ چیز نچسبیده. ممبران لبهٔ خام ندارد — فویل دورش پرس شده (۵.۲) — و شمرده نمی‌شود.
  • از صحنه به رأس: useRawEdges (components/scene/use-raw-edges.ts) فقط وقتی نما روشن است حساب می‌کند و هر لبه را cutId#اندیس می‌نامد؛ Room برای هر ورق (PartBoard) نوارها، مغز و این مجموعه را به PartBox می‌دهد و faceLooks لبهٔ خامِ دیدنی را نگاهِ چهارم (seen) می‌کند. گروهِ آن همان گروهِ مغز است (groupOf)، پس شمارِ رسم عوض نمی‌شود؛ کلیدِ انبارهٔ هندسه نگاهِ seen را دارد و فقط ورقی که لبهٔ دیدنیِ خام دارد هندسهٔ دیگری می‌گیرد.
  • یک یکنواخت، نه جنسِ دیگر: BoardMaterial میانِ رنگِ عادی و رنگِ نما می‌آمیزد با edgeView، یکنواختی در renderGroup که setEdgeView می‌نشاند. همان برنامه‌ها می‌مانند: روشن و خاموش کردن هیچ برنامه‌ای نمی‌سازد و جنس‌های ورق دست نمی‌خورند.
  • شمارش: RawEdgeCount کنارِ منو (role="status"، data-raw-edges): «۱۲ لبهٔ خامِ دیدنی» یا «لبهٔ خامِ دیدنی نیست»؛ نما خاموش باشد، هیچ.
  • پذیرش (بیلد، WebGPU، «بالا»): آشپزخانهٔ نمونه ۰ نارنجی و دستیار ۰؛ همان آشپزخانه با نوارهای «دودرب» برداشته ۱۲ = ۱۲ (بدنهٔ چپ، کف، کلاف و طبقه هر کدام لبهٔ جلو، و چهار لبهٔ هر در؛ لبهٔ جلوی بدنهٔ راست فارسی‌بر است و لبهٔ خام نیست). رسم ۵۶۶ = ۵۶۶، مثلث ۱۶۱٬۴۲۸ = ۱۶۱٬۴۲۸، برنامه ۱۲۲ = ۱۲۲؛ هندسه ۱۶۶ ← ۱۶۸ فقط در پروژهٔ بی‌نوار. آزمون: core/test/raw-edge.test.ts (شمارِ قاعده = شمارِ رنگ‌شده در سه گونه، ممبران)، web/test/boxgeo.test.ts (نگاهِ seen در گروهِ مغز)، display-menu.dom.test.tsx (گزینه و شمارش).
  • سرِ راه: روشن کردنِ نما، مثلِ هر تغییرِ «نمایش»، کاوشگرهای بازتاب را دوباره می‌گیرد و هر بار ۱۶ تا ۱۸ خطای WebGPU می‌داد؛ ریشه و درمان در «بازتابِ فلز» (بالا)، آزمون web/test/reflection-probe.test.ts.
  • «برش» در سه جهت (۴.۵): عمق (از اتاق به سوی دیوار — داخلِ کشو و پشت‌بند)، پهنا (از راستِ آشپزخانه — همهٔ بدنه‌ها کنارِ هم) و ارتفاع (از بالا — طبقه‌ها از بالا). جهت کلیدِ سه‌تایی کنارِ خودِ لغزنده است (SceneTools → SectionAxis)؛ وضعیت در scene/use-section.ts. مقدار میلی‌متر است با گامِ ۱، نه درصد: گامِ ۱٪ در ارتفاعِ آشپزخانه ۲۲ میلی‌متر بود و ورقِ ۱۶ میلی‌متری میانِ دو گام جا می‌ماند. خروجیِ لغزنده جای برش را می‌گوید، از کف، دیوار یا سرِ رده (۸۱۰ میلی‌متر)، یا «خاموش». گردشِ جهت همان سهم از آشپزخانه را در جهتِ تازه برمی‌دارد.
  • صفحه (scene/Clipping.tsx): ClippingGroup ِ three (رندرکنندهٔ WebGPU clippingPlanes ِ سراسری ندارد) دورِ آشپزخانه و دیوارها، نه کف، نه نشانه‌های اندازه، نه درپوش‌ها؛ سایه‌ها را هم می‌بُرد. کف بریده نمی‌شود: با برشِ عمق، کفِ جلو با نماها می‌رفت و آشپزخانه روی خاکستریِ پس‌زمینه معلق می‌ماند. صفحه همیشه هست و فقط جابه‌جا می‌شود: شمارِ صفحه‌ها بخشی از کلیدِ شیدرِ هر جنس است و افزودنش همهٔ خطِ لوله‌ها را در اولین لمسِ لغزنده دوباره می‌ساخت.
  • درپوش (scene/SectionCaps.tsx): هر ورقی که صفحه از آن می‌گذرد با مستطیلی به رنگِ مغزِ ورق (CORE در scene/materials.ts: MDF قهوه‌ای، نئوپان روشن‌تر، تخته‌لایه بِرچ، HDF تیره) بسته می‌شود؛ صفحهٔ کار به رنگِ سنگش، درِ دستگاهِ توکار به استیلش، یراق بی‌درپوش (lib/section/faces.ts). بی درپوش ورقِ بریده جعبه‌ای توخالی بود و ضخامتِ طبقه خوانده نمی‌شد. درپوش تحلیلی است، نه استنسیل: هر قطعه جعبه‌ای ایستاده روی کف است که با دیوارش حولِ محورِ قائم چرخیده (rotY)، پس برخوردِ صفحه با آن مستطیل است (lib/section/caps.ts ← capOf) — روی دیوارِ دومِ L هم. همه در یک mesh با رنگِ هر گوشه؛ فقط در حالتِ plain (باز یا منفجر، قطعه از جعبه‌اش دور می‌شود) و بیرون از عکس. پذیرش: ضخامتِ درپوش = ضخامتِ ورق، روی آشپزخانه‌های نمونه (test/section-caps.test.ts).
  • «اندازه» (۴.۴): کلیک روی قطعه، نه جایی که پرتو خورد، بلکه نزدیک‌ترین نقطه از ۲۶ نقطهٔ جعبهٔ خودِ قطعه را روی صفحه می‌گیرد — ۸ گوشه، وسطِ ۱۲ لبه، مرکزِ ۶ رویه (lib/measure/snap.ts ← OFFSETS، nearestOffset؛ scene/snap-under.ts جعبه را با matrixWorld ِ همان مش روی صفحه می‌برد) — و گوشه اندکی پیش از وسطِ لبه. پرتو یک کسر از میلی‌متر کنارِ گوشه می‌نشست؛ حالا اندازه عددِ خودِ قطعه‌هاست: خطای ۰ روی همهٔ لبه‌های همهٔ قطعه‌های صاف و ال (test/measure-snap.test.ts). سرِ اندازه نقطه‌ای از قطعه است (MeasureEnd = شناسهٔ قطعه + جای روی جعبه)، نه جایی در اتاق: با ویرایش، اندازه با قطعه جابه‌جا می‌شود، با رفتنِ قطعه می‌رود (و با واگرد برمی‌گردد)، و در «بازشده» همراهِ قطعه بیرون می‌رود (explodedCenter). در «درها باز» نمای بازِ ایستاده از جعبه‌اش دور است و نقطه نمی‌گیرد. اندازه‌ها می‌مانند — سومی دیگر قبلی را پاک نمی‌کند — تا «پاک کردنِ N اندازه» ِ کنارِ همان کلید؛ کلیک روی جای خالی نقطهٔ اولِ نیمه‌کاره را رها می‌کند. وضعیت در scene/use-measures.ts؛ کشیدن در scene/Measure.tsx (خط با Line2 ِ WebGPU ِ three — Line ِ drei ShaderMaterial بود که این رندرکننده اجرا نمی‌کند — نقطهٔ زیرِ نشانگر کم‌رنگ و بزرگ‌تر، از نقطهٔ اول تا آن خطِ زنده با طولش، برچسب با Html ِ drei). برچسبِ Html در صفحهٔ راست‌به‌چپ لبهٔ راستش را روی نقطه می‌گذاشت و center نیمِ پهنایش را کم می‌کرد: هر برچسب یک پهنای کامل چپِ خطش بود؛ AT_POINT (scene/at-point.ts) لبهٔ چپ را روی نقطه می‌گذارد، برای برچسبِ مانع‌ها هم (test/html-anchor.test.ts).

scene/FinishesPanel.tsx روی room.finishes (رنگ دیوار، کف: نوع و رنگ)؛ صحنه (scene/RoomSurfaces.tsx) دیوار را گچِ رنگ‌شده (plaster) و کف را به نوعش می‌سازد: چوب → پارکتِ تخته‌ای (planks: تختهٔ ۲۰۰ × ۱۲۰۰ با درزِ پخ‌دار، سرِ تخته‌های هر ردیف جای دیگر)، سرامیک → کاشیِ ۶۰۰ × ۶۰۰ با بندکشیِ ۳ میلی‌متری (tiles)، سنگ → اسلبِ ۱۲۰۰ × ۶۰۰ با درزِ مویی و ردیف‌های نیم‌گام (slabs)، بتن → بتنِ صیقلی (concrete). UV ِ دیوار و کف به متر است (metricUv)، نه repeat ِ ثابت: repeat ِ ۱۲ از روزی مانده بود که کف بیست متر بود، و کاشیِ ۱٫۶ متری روی کفِ ۳٫۲ × ۳٫۵ متری ۰٫۲۷ × ۰٫۲۹ متر و ناهم‌سو می‌شد. ویرایش‌های پیاپی یک قدم واگرد.

سطح‌ها و بافت‌ها (ممیزیِ نسل هفتم)

Section titled “سطح‌ها و بافت‌ها (ممیزیِ نسل هفتم)”
  • هر سطح یک SurfacePattern است (lib/textures/pattern.ts): الگو، کاشیِ فیزیکی به متر در امتدادِ u و v (در ورقِ رگه‌دار u در امتدادِ رگه است و کاشیِ بلوط ۱ × ۰٫۵ متر: ریزتر در عرضِ رگه، جایی که ریزه‌کاری است)، شمارِ تکسل، و برجستگی به میلی‌متر. نقشهٔ نرمال شیبِ خودِ ورق است — ارتفاع به میلی‌متر بر تکسل به میلی‌متر (bake.ts) — و normalScale نیست. پیش از ممیزی بهرهٔ برجستگی ثابت بود و با چشم رام می‌شد: ملامینهٔ سفید نور را از تکسل به تکسل ۴٫۹ درجه کج می‌کرد (گچِ زبر از یک متری)، بلوط ۲۵، ام‌دی‌اف خام ۲۳، تخته‌چندلا ۱۹، کوارتزِ صیقلی ۳. حالا ۰٫۳۶، ۰٫۵۴، ۰٫۴۹، ۰٫۴۴ و ۰٫۰۹ (RMS، test/texture-truth.test.ts).
  • نوفه بی‌درز است: fbm(u, v, fx, fy, …) شمارِ خانه‌های کاشی را می‌گیرد و همان را دوره می‌کند، پس الگو جز عددِ درست نمی‌تواند بخواهد. پیش‌تر الگوها نوفه را با بسامدی جز دوره‌اش می‌خواندند (۲۱۰ خانه روی شبکهٔ ۵۱۲) و هر بافت در هر تکرار درز داشت؛ UV ِ قطعه از مرکزش شروع می‌شود، پس درز از وسطِ هر در و نمای کشو می‌گذشت.
  • سبزِ نقشهٔ نرمال: سطرهای بوم رو به پایین‌اند و v رو به بالا (flipY)؛ علامتش پیش‌تر وارونه بود. بایتِ نرمال با لرزش گرد می‌شود (toByte): یک پلهٔ بایت ۰٫۴۵ درجه است و پوست‌پرتقالیِ های‌گلاس (۰٫۱۲) کمتر از آن.
  • میانگینِ رنگِ هر سطح، در نورِ خطی، رنگِ کاتالوگ است (albedoOf): تُن‌ها کانال‌به‌کانال مقیاس می‌خورند؛ کارتِ رنگ (e2e/color.spec.ts) همین را روبه‌روی نمونه می‌سنجد.
  • رگه: PartInstance.grain محورِ جعبهٔ قطعه است که length ِ لیست برش در آن می‌دود — frontGrainVertical ِ هسته: درِ لولاپهلو، لنگهٔ کنج و نمای دستگاه عمودی، نمای کشو و درِ بالابر افقی؛ قابِ کلاسیک پایه و صفحه عمودی، تراور افقی (packages/geometry/test/grain.test.ts روی همهٔ پیکره‌ها). بی آن، بدنه و پهلو ایستاده و بقیه در امتدادِ بلندترین ضلع (grainAxisFor). پیش‌تر صحنه رگهٔ همهٔ نماها را ایستاده می‌کشید و لیست برش رگهٔ نمای کشو را افقی می‌برید. هر قطعهٔ رگه‌دار از یکی از هشت جای ورق شروع می‌شود (sheetOffset)، تا درهای هم‌عرض کپیِ هم نباشند و انبارهٔ هندسه هنوز هر اندازه را هشت‌بار به اشتراک بگذارد.
  • زمانِ پخت در Chrome: ورق‌ها ۲۳ تا ۵۷ میلی‌ثانیه، کف‌ها در ۷۶۸² ۶۰ تا ۹۷ (در ۱۰۲۴² تا ۱۴۳ بود).

حالت «مونتاژ» در نوار: assemblySteps(unit, cat) برای یونیت انتخاب‌شده (از راهِ stepsOf ِ scene/assembly.ts؛ یونیتِ ناساختنی بی گام)؛ rolesUpTo(steps, i) نقش‌های سوارشده تا گام جاری را می‌دهد و بقیهٔ قطعات همان یونیت شفاف می‌شوند (PartBox.dimmed). قبلی/بعدی در SceneTools.

دستگاه‌های توکار پارامتری (V4)

Section titled “دستگاه‌های توکار پارامتری (V4)”

یونیتی که appliance دارد (فر، یخچال، مایکروویو، ماشین ظرف‌شویی) در صحنه یک «نمای دستگاه» می‌گیرد: جعبه‌ای به اندازهٔ دهانهٔ یونیت منهای ده میلی‌متر لقی از هر طرف، هجده میلی‌متر جلوی بدنه. هندسه در packages/geometry/src/fixtures.ts (applianceFixtures) ساخته می‌شود و ظاهرش در apps/web/src/components/FixturesAppliances.tsx از یک جدول کوچک می‌آید: پنجرهٔ شیشه‌ای، دستگیرهٔ افقی یا عمودی، نوار کنترل. مدل دانلودی و بافت نداریم — آفلاین می‌مانیم و اندازه از خودِ یونیت می‌آید.

رندر: لغو، نگه‌داشتن در پروژه، جلد گزارش (V6)

Section titled “رندر: لغو، نگه‌داشتن در پروژه، جلد گزارش (V6)”
  • لغو: RenderRequest.signal بین دو نمونهٔ انباشت بررسی می‌شود و با AbortError بیرون می‌آید؛ پنل رندر دکمهٔ «لغو» را هنگام کار نشان می‌دهد.
  • نگه‌داشتن در پروژه: project.renders (نوع SavedRender) تا سقف شش رندر؛ هر کدام فقط assetId دارد — بایت در AssetRepo ِ مرورگر و در assets/ ِ پروندهٔ .darz، که همچنان خودکفاست (data-model.md، «تصویر بیرون از سند»). فهرست با بندانگشتی، «جلد گزارش» و «حذف» زیر تنظیمات رندر می‌آید.
  • جلد گزارش: coverOf(project, url) در بستهٔ گزارش — url شناسهٔ تصویر را به data URL می‌رساند (وب از AssetRepo، خط فرمان از assets/ ِ .darz) — رندرِ علامت‌خورده (یا تازه‌ترین) را به ReportInput.cover می‌دهد و صفحهٔ اول گزارش آن را نشان می‌دهد. وب و CLI هر دو همین را می‌فرستند.

رندرِ آفلاین (نسل هفتم، ۲.۶)

Section titled “رندرِ آفلاین (نسل هفتم، ۲.۶)”
  • همان موتور: exportJob (lib/render/job.ts) همان گذرهای نمای زنده را (scenePasses) در اندازهٔ خروجی می‌کشد، بی خطِ دورِ انتخاب و با انسدادِ تمام‌تفکیک. هر نمونه در دوربین (نمای روبه‌روی متعامد هم — بی ابرنمونه تنها ضدِّدندانهٔ آن است)، دیافراگم و نورِ کلید لرزانده می‌شود، و میانگین روی کارتِ گرافیک در دو هدفِ شناورِ ۳۲بیتی نگه داشته می‌شود (Accumulator ِ lib/gpu/accumulate.ts)؛ نگاشتِ Neutral یک بار، روی میانگین؛ بعد خواندنِ ناهمگامِ هدفِ هشت‌بیتی (lib/gpu/readback.ts: WebGPU سطرها را تا ۲۵۶ بایت پر و از بالا می‌دهد، WebGL2 فشرده و از پایین) و PNG. lib/ao/، grade.ts، blur.ts و انباشتِ هشت‌بیتیِ بوم رفتند.
  • کیفیت‌ها (components/render/presets.ts): «پیش‌نویس» ۴ نمونه، «خوب» ۳۲، «بهترین» ۹۶ — یک عدد؛ supersample رفت، چون هر نمونه در پیکسلِ خودش می‌لرزد.
  • همگرایی (e2e/render-accumulate.spec.ts، کارتِ گرافیک): RMSE ِ ۶۴ نمونه تا مرجعِ ۱۰۲۴ نمونه ۰٫۴۸۰ سطح و ۲۵۶ نمونه ۰٫۲۰۹ (نسبتِ ۲٫۳۰؛ پذیرش > ۱٫۸)، هر دو شاخه یکسان. انباشتِ هشت‌بیتیِ پیشین در ۲۵۶ نمونه ۳٫۰۱ سطح دور بود.
  • زنده و خروجی یکی (e2e/color.spec.ts): هر درِ کارتِ رنگ در نمای «روبه‌رو» ِ زنده و در خروجی — میانگینِ ΔE2000 ۰٫۳۳ (SwiftShader، «حداقل») و ۰٫۳۳ (WebGPU، «بالا»)؛ پذیرش ≤ ۱٫۵.
  • هر نمونه یک فریم (advanceFrame ِ lib/gpu/frame.ts): گذرها و انسداد یک بار در هر فریمِ سامانهٔ گره‌ای به‌روز می‌شوند و فریم را حلقهٔ requestAnimationFrame ِ رندرکننده می‌شمارد؛ حلقه‌ای که در یک تیک چند بار رندر کند، تصویرِ اول را پس می‌گرفت. تنها دستِ درازِ ما به درونِ three (renderer._nodes)، در زمانِ اجرا سنجیده و با آزمونِ همگرایی نگه داشته. شمارهٔ فریمِ هر نمونه باقی‌ماندهٔ ۱۲ ِ خودِ نمونه را دارد: GTAO نوفه‌اش را با شمارهٔ فریم می‌چرخاند و حلقهٔ نمایش در نفس‌ها هم می‌شمارد — ۲۵۶ نمونه یک بار ۰٫۲۱ و بارِ بعد ۰٫۳۸ دور بود.
  • اندازه و آرامش: نمای زنده هنگامِ خروجی فریم نمی‌کشد (holdFrames)؛ بافرِ رسم پیش از هر نمونه به اندازهٔ خروجی برمی‌گردد (holdSize: پنجره یا رده‌ای با نسبتِ دیگر وسطِ کار بوم را عوض می‌کرد)؛ و خروجی پیش از شروع تا دو فریمِ نمای زنده صبر می‌کند (sceneSettled، سقف سه ثانیه) — صحنه یک فریم دیرتر از پروژه می‌آید و رندرِ درست پس از باز کردن (darzsaz render، آزمون) آشپزخانه‌ای با کابینت‌های دیواریِ ناتمام را قاب می‌گرفت. SceneGuard بوم را با اندازهٔ CSS و نسبتِ خودش برمی‌گرداند؛ پیش‌تر پهنای بافر را به‌جای CSS می‌داد و در نسبتِ ۲ بوم دو برابر پهن برمی‌گشت.
  • نفس: حلقهٔ انباشت میان دو نمونه با breathe() ِ lib/render/breathe.ts (یک پیامِ MessageChannel) نفس می‌کشد، نه setTimeout(0) — زمان‌سنجِ زبانهٔ پنهان به ≥ ۱ ثانیه کُند می‌شود؛ هر چهار نمونه یک بار منتظرِ کارتِ گرافیک (gpuSync) تا نوارِ پیشرفت مالِ کارت باشد نه صف.
  • خطاهای بوم و تصویر DarzError با پیام‌اند (lib/render/errors.ts)، نه متنِ فارسی.

رندرِ واقع‌گرا (نسل هفتم، فاز ۳)

Section titled “رندرِ واقع‌گرا (نسل هفتم، فاز ۳)”

پنجرهٔ «رندر» دو موتور دارد: سریع (همان گذرهای نمای زنده، بالا) و واقع‌گرا که نور را ردیابی می‌کند (lib/photo/) — بازتابِ اتاق در درِ براق، سایه‌ای که با اندازهٔ نورش نرم است، نوری که میانِ کابینت‌ها برمی‌گردد.

  • موتور: three-gpu-pathtracer 0.0.24 روی یک WebGLRenderer ِ جدا و بیرون از صفحه (نمای زنده WebGPU است و این ردیاب فقط WebGL). تکهٔ تنبلِ photo-*.js (≈ ۱۶۷ کیلوبایت gzip) با three-mesh-bvh؛ هیچ بایتش در Scene3D نیست. افزونهٔ vite/three.ts three را همه‌جا به three/webgpu و فقط در src/lib/photo/ به ساختِ کامل می‌برد — هر دو three.core.js را شریک‌اند؛ جای resolve.alias ِ پیشین که به همهٔ پرونده‌ها یکی می‌داد.
  • صحنه (scene.ts): هر مش با ماتریسِ جهانیِ زنده و نسخه‌ای از هندسه‌اش (ردیاب در هندسه صفت می‌نویسد)؛ ابزارهای ویرایشگر (PHOTO_SKIP)، پنهان‌ها و خطِ چاق بیرون. جنس‌ها (materials.ts): ردیاب روی macOS (Metal زیرِ ANGLE) هر clearcoat را سیاه سایه می‌زند (gkjohnson/three-gpu-pathtracer#711، بسته و بی‌اصلاح)؛ پوششِ ≥ ۰٫۵ خودِ رویه می‌شود (زبریِ پوشش، بازتابِ کامل)، پوششِ نازک برداشته. رنگِ رأسِ «شستشوی دیوار» برداشته — ردیاب افتِ نور را خودش دارد. دیوارهای اتاق سایه نمی‌اندازند (castShadow ِ جنسِ ردیاب، shadowless)، چون در نمای زنده هم نمی‌اندازند؛ ردیاب با دیوارِ کاملِ اتاق آشپزخانه را جعبه‌ای می‌کرد که فقط از سقفِ باز نور می‌گرفت.
  • نور (lights.ts): روز همان نورهای کالیبرهٔ نمای زنده، هر نورِ جهت‌دار یک سافت‌باکسِ دور با همان تابش (L = E ÷ Ω، با انتگرال آزموده) — بی نورِ پرکننده، که جای نورِ برگشتی بود و ردیاب خودش ردیابی‌اش می‌کند. عصر آفتابِ کم و گرم از سمتِ کلید و نورِ اتاق یک‌سوم به رنگِ آفتاب؛ شب نوارهای LED (مستطیلِ نور زیرِ هر نوار، ۰٫۴ ِ نورِ واحد روی صفحهٔ کار) و چراغِ سقف، با نوردهیِ بلندتر نه نورِ بیشتر. «نورِ واحد» در واحدِ تابشِ three π است (FRONT_IRRADIANCE: سطحِ سفیدِ پخشی E ÷ π را پس می‌دهد، و شدتِ کلید سهم × π ÷ cosθ است)؛ تا ممیزیِ نسل هفتم سهمِ نوار و چراغِ سقف خودِ تابش گرفته می‌شد و در عکسِ روز نوارها ۰٫۴ ÷ π ِ نورِ واحد می‌دادند و دیده نمی‌شدند (دیوارِ زیرشان روشن در برابرِ خاموش +۱ تا +۷ سطح). حالا دو موتور یکی‌اند (+۲۳ و +۱۷ هم‌ترازِ نوار، +۷ و +۸ در ۱۹ سانت)؛ نوردهیِ شب ۲٫۵ ÷ π تا عکسِ شب همان بماند. درخشندگیِ هر نوار از پهنای مستطیلِ خودش است.
  • کارتِ خاکستریِ روز (meter.ts، card.ts): یک کارتِ سفیدِ کاملاً پخشی جلوی وسطِ آشپزخانه، رو به اتاق، که خودِ ردیاب سایه‌اش می‌زند و دوربینی جز آن نمی‌بیند؛ بهره هر کانال یعنی نورِ واحد ÷ آنچه کارت پس می‌دهد، با نوردهیِ ۱٫۰ (TRACED_EXPOSURE) نه ۱٫۱۶ ِ نمای زنده — Neutral ۴٪ را برای بازتابِ دی‌الکتریک کم می‌کند و ردیاب آن بازتاب را واقعاً دارد؛ ۱٫۱۶ جبرانِ اتاقِ کم‌نورِ پشتِ دوربینِ نمای زنده است. کارتِ رنگِ ردیابی‌شده (e2e/traced.spec.ts، ۶۴ نمونه): میانگین/بیشینهٔ ΔE2000 ۱٫۶۶/۳٫۹، میانگینِ ‎ΔL*‎ −۱٫۱ (پیش از کارت ۵٫۷، سفیدها #F1E5D6). امتحان شد و کنار رفت: کلیدِ ۱٫۶ برابر و اتاقِ ۰٫۶ (سایه خواناتر، کارت ۳٫۷)، و انتگرالِ آنچه دوربینِ استوانه‌ای در جای کارت می‌بیند (نورِ رسیده به یک نقطه، نه آنچه سایه‌زنیِ ردیاب از یک رویه می‌سازد).
  • ردیاب برای هر ردیابی تازه (engineOf، release): ردیابی که صحنهٔ دوم یا دوربین و اندازهٔ تازه بگیرد غلط سایه می‌زند — کارت در یک صفحه ۰٫۷۴، ۰٫۶۰، ۰٫۱۸، ۰٫۶۰ خواند؛ تازه‌اش هر بار ۰٫۷۳ ± ۰٫۰۱، و یک دهمِ ثانیه بیشتر (سایه‌زنِ کامپایل‌شده در انبارِ مرورگر است). release آنچه dispose ِ خودش جا می‌گذارد آزاد می‌کند (BVH، آرایهٔ بافت، جنس‌ها، نورها، جدول‌های نورِ اتاق). نمونه‌ها بذرخورده‌اند (stableNoise): یک صحنه، یک تصویر.
  • عددِ تصادفیِ هر پیکسل از خودش (PCG، RANDOM_TYPE ۰): فهرستِ طبقه‌بندیِ پیش‌فرضِ ردیاب یک دنباله در هر فریم برای همهٔ پیکسل‌هاست که با یک عددِ نوفهٔ آبی جابه‌جا می‌شود؛ با چند ده فریم خطایش روی یک دیوارِ کامل هم‌جهت می‌ماند و حذف‌نوفه آن را نوفه نمی‌بیند. عکسِ «خوب» تا مرجعِ ۴۰۹۶ نمونه: ۳٫۸٪ ← ۲٫۸۵٪، و خطای بلوک‌های ۸ × ۸ ۲٫۹٪ ← ۰٫۹۵٪. Sobol روی macOS کامپایل نمی‌شود (یادداشتِ خودِ ردیاب).
  • حذف‌نوفه (denoise.ts): Open Image Denoise با oidn-web 0.4.0 روی WebGPU، با آلبدو و نرمالِ رسم‌شده (aov.ts، MSAA ۴)؛ وزن‌ها RenderKit rt_hdr_alb_nrm_small (۶۴۱ کیلوبایت، Apache-2.0) در public/oidn/ که بارِ اول دریافت و انبار می‌شوند و در پیش‌انبارِ برنامه نیستند. NaN ِ ردیاب پیش از شبکه صفر می‌شود (finite) — یک NaN کلِ کاشیِ شبکه را سیاه می‌کرد — و کاشیِ سیاه‌برگشته یک بار دوباره و بعد تصویرِ خام. بی WebGPU حذف‌نوفه نیست و بودجهٔ زمان سه برابر (UNDENOISED).
  • بودجهٔ زمان (PHOTO_QUALITY): «پیش‌نما» ۵ ثانیه (کمینه ۸ نمونه)، «خوب» ۳۰ (۳۲)، «بهترین» ۹۰ (۹۶)؛ شمارِ نمونهٔ دقیق (samples ِ خودکارسازی، --samples) بودجه را کنار می‌گذارد. پیش‌نمای اول تصویرِ ۴ نمونه‌ای ِ موتورِ سریع است، بعد هر ثانیه تصویرِ ردیاب؛ روی عکس، پیش‌نما هم روی عکس.
  • لنز (۳.۵، lib/render/lens.ts، lenses.ts): ۲۴، ۳۵ و ۵۰ میلی‌متر روی قاب کامل (زاویهٔ دید ۲·atan(۱۲/f))؛ «عمودها صاف» دوربین را تراز نگه می‌دارد و قاب را با جابه‌جاییِ لنز می‌لغزاند (ShiftCamera، levelFrame)، مثلِ عکسِ معماری. عمقِ میدان همان را دارد (لنزِ نازک با جابه‌جایی؛ در ردیاب PhysicalCamera). «نمای فعلی» زاویه را از خطِ دیدِ وسطِ قابِ نمای زنده می‌گیرد (lineOfSight): نمای زندهٔ تراز (۴.۳) محورش افقی است و قابش لغزیده.
  • سایه روی عکسِ دیوار (۳.۶): موتورِ سریع دیوار و کف را ShadowMaterial می‌کند (SceneGuard). ردیاب آشپزخانه را میانِ دیوار و کف ردیابی می‌کند و بعد اتاقِ تنها، و روی دیوار و کف فقط نسبتِ این دو را پس می‌دهد: سیاه به کدریِ نوری که آشپزخانه برد (catcher.ts، رندرِ تفاضلیِ Debevec) — سایهٔ افتاده و تیرگیِ نرمِ زیرِ کابینت‌های دیواری. نسبت در برابرِ پرتکرارترینِ نسبتِ دیوار (قلهٔ هیستوگرام) سنجیده می‌شود، نه روشن‌ترین دهک که با نوفه بالا می‌رفت و کلِ عکس را ۸ تا ۱۲٪ تیره می‌کرد. کدریِ هر پیکسلِ نیمه‌کابینت‌نیمه‌دیوار از پوششِ رسم‌شده (drawCoverage).
  • امضای کارگاه (۳.۷، lib/render/signature.ts): آرم و نامِ کارگاه (صفحهٔ «کارگاه» ِ مدیریت) روی نواری نیمه‌شفاف در گوشهٔ پایینِ پایانِ سطرِ زبانِ برنامه، به اندازهٔ ارتفاعِ تصویر؛ آنچه نگه داشته و ذخیره می‌شود تصویرِ امضاشده است.
  • پذیرش (۳.۹، مکِ همین مخزن، ۱۸۰۰ پیکسل، آشپزخانهٔ نمونه، روز): پیش‌نمای اول ۸۵۵ میلی‌ثانیه، «لغو» در ۹۶۷؛ فاصله تا مرجعِ خامِ ۴۰۹۶ نمونه (۲۵ دقیقه) به RMSE ِ نسبیِ کدهای sRGB: «خوب» ۲٫۸۵٪ در ۳۳ ثانیه، «بهترین» ۲٫۴۵٪ در ۹۵، بی حذف‌نوفه ۷٫۲٪ در ۹۲ — هدفِ ۲٪ در ۳۰ ثانیه نرسید. مرجع خودش ≈ ۱٫۱٪ نوفه دارد؛ باقی خطای حذف‌نوفه روی لبه‌هاست (یک‌دهمِ پیکسل‌ها نیمی از خطا). امتحان شد و کنار رفت: مدل‌های دیگرِ OIDN (calb_cnrm کوچک ۳٫۳٪، استاندارد ۳٫۱٪) و راهنماهای حذف‌نوفه با همان پیکسلِ خیمه‌ایِ ردیاب (۲٫۸۲٪، بی اثر).
  • سه عکسِ مرجع (۳.۳): docs/images/render-{day,evening,night}.jpg، از darzsaz render --gpu --engine path --light <نور> --quality good روی آشپزخانهٔ نمونه.
  • خطِ فرمان (۳.۸): darzsaz render --gpu --engine path --samples N، با --light، --lens و --no-level (cli.md).

نور و رنگ (نسل هفتم، فاز ۱)

Section titled “نور و رنگ (نسل هفتم، فاز ۱)”

قاعده همان کالیبره‌کردن با کارتِ خاکستری است: رویه‌ای که رو به اتاق است دقیقاً نورِ واحد می‌گیرد (scene/calibration.ts)، و در سایه‌زنیِ فیزیکیِ three رویهٔ پخشیِ ρ همان ρ را پس می‌دهد.

  • محیط: RoomEnvironment ِ خودِ three (lib/env.ts، roomEnvironment)، نه محیطِ دست‌سازِ پیشین با «پنجره»ی شدتِ ۲۲. آنچه به رویهٔ سفید می‌رساند سنجیده شد (سطحِ سفید در هدفِ شناور، بی نگاشت، three 0.186.0): رو به اتاق (‎+z) ۱٫۸۶۵، بالا ۱٫۴۴۷، دو پهلو ۰٫۹۸۰ و ۰٫۷۸۴، زیر ۰٫۵۹۴، پشت ۰٫۶۷۸ (ROOM_IRRADIANCE). هر ارتقای three دوباره می‌سنجدش: 0.186 اتاق را جورِ دیگری پیش‌پالایش می‌کند و رویهٔ افقی ۱۰٪ کم‌نورتر شد.
  • سهم‌ها: محیط ۵۵٪ ِ نورِ روبه‌رو (ENV_SHARE، پس environmentIntensity = ۰٫۵۵ ÷ ۱٫۸۶۵)، نورِ کلید ۳۰٪ (keyIntensity: شدت = سهم × π ÷ cosθ، از هر جا که بایستد)، پرکنندهٔ بی‌سایه از جلو-راست ۱۵٪ (fillIntensity، جای نورِ برگشتی‌ای که نمای زنده ندارد — بی آن دیوارِ چپ و پهلوی راستِ کابینت فقط ۰٫۳۵ ِ محیط را داشتند؛ رندرِ واقع‌گرا آن نور را ردیابی می‌کند و پرکننده را ندارد)، و نورِ بالا ۲۰٪ ِ یک رویهٔ افقی. نورِ محیطیِ ثابت رفت: محیط کارش را می‌کند. هر سه نور سفیدِ خنثیاند (LIGHT): رنگِ کاتالوگ زیرِ همین‌ها داوری می‌شود.
  • نگاشت: Khronos PBR Neutral با نوردهیِ ۱٫۱۶ (EXPOSURE)، از جاروبِ کارتِ رنگ: میانگین/بیشینهٔ ΔE ِ Neutral ۱٫۰۰ ← ۲٫۹۷/۵٫۰، ۱٫۰۸ ← ۱٫۷۳/۲٫۹، ۱٫۱۲ ← ۱٫۵۱/۲٫۱، ۱٫۱۶ ← ۱٫۴۴/۲٫۳؛ ACES ۰٫۹۲ ← ۴٫۲۱/۶٫۴؛ AgX ۱ ← ۷٫۰۷/۱۱٫۶. Neutral ۴٪ را برای بازتابِ دی‌الکتریک زیرِ محیطِ یکدست کم می‌کند؛ در این اتاق رویهٔ روبه‌رو دیوارِ کم‌نورِ پشتِ دوربین را بازتاب می‌دهد، کمتر از آن، و بی جبران هر در ۱٫۶ تا ۵٫۱ ‎L*‎ تیره بود.
  • استیل برس‌خورده (HOOD_STEEL و APPLIANCE_STEEL زبریِ ۰٫۴۲؛ سینک ۰٫۴۰): با ۰٫۲۶ از روبه‌رو فقط دیوارِ کم‌نورِ پشتِ دوربین را آینه می‌کرد و سیاه دیده می‌شد؛ از ممیزیِ نسل هفتم همهٔ فلزها خودِ اتاق را آینه می‌کنند (ReflectionProbe، بالا). شیشهٔ گاز بی reflectivity ِ دست‌کاری‌شده: شیشهٔ سرامیکیِ ساده.
  • جنس: هیچ جنسی شدتِ محیطِ خودش را ندارد (envMapIntensity ِ هر خانواده رفت: های‌گلاس ۱٫۳۵ و ملامینه ۰٫۸۵ کالیبره را می‌شکستند). رویهٔ پوشش‌دار از پوششش بازتاب می‌دهد، نه دو بار: specularIntensity = 1 − clearcoat (coatedSpecular).
  • خروجی: منحنیِ S، تفکیکِ سرد و گرم و ویگنتِ grade.ts رفتند — رنگِ ورق را عوض می‌کردند و نمای زنده نداشتشان. شکوفه از فاز ۲ فقط از رویه‌های نورده است (هدفِ emissive ِ گذرِ صحنه، BloomNode) و در هر دو نما یکی.

رنگ و سنجه (نسل هفتم، ۰.۳–۰.۵)

Section titled “رنگ و سنجه (نسل هفتم، ۰.۳–۰.۵)”
  • کارتِ رنگ (colorBooth در packages/core/src/fixtures/color-booth.ts): یک دیوار، یک درِ ساده برای هر رنگِ نمای کاتالوگ (پشت‌بند نه، هر رنگ یک بار)، بی دستگیره و بی نوار. e2e/color.spec.ts آن را روبه‌رو رندر می‌کند؛ دوربینِ روبه‌رو دقیقاً دیوار را قاب می‌گیرد، پس مرکزِ هر در به میلی‌متر جای آن در در تصویر است. پایه (پیش از فاز ۱، SwiftShader، «خوب»): میانگینِ ΔE2000 ۱۳٫۵، بیشینه ۲۵٫۵ (های‌گلاس طوسی)، ‎ΔL*‎ تا ۳۲٫۸.
  • شمارنده‌ها: RenderApi.info() یک فریمِ خطِ لوله می‌کشد و renderer.info ِ همان را می‌دهد (رندرکنندهٔ WebGPU شمارنده‌هایش را در هر فریمِ نمایش صفر می‌کند)؛ RenderApi.bench(n) فریم‌های پشتِ همِ خطِ لوله، هر کدام فریمی از خودش، با انتظارِ کارتِ گرافیک (gpuSync: یک پیکسل پاک و خوانده می‌شود — waitForGPU از r181 نیست)؛ RenderApi.frames() شمارِ فریم‌های خطِ لوله. همه فقط زیرِ خودکارسازی به window می‌رسند (components/render/automation.ts)؛ __darzsazRender یک samples ِ دلخواه (۱ تا ۴۰۹۶) هم می‌پذیرد.

عکسِ دیوار: Worker، جهتِ EXIF، مهار (۷.۱۲، V25)

Section titled “عکسِ دیوار: Worker، جهتِ EXIF، مهار (۷.۱۲، V25)”
  • راست‌سازی در Worker: lib/rectify.ts فقط ریاضی و پیکسل است (rectifyPixels)؛ lib/rectify.worker.ts با OffscreenCanvas می‌خواندش و پیکسل‌های منبع را برای کشیدنِ بعدیِ گوشه نگه می‌دارد. Rectifier ِ lib/rectify-client.ts صفِ «آخری می‌برد» با درنگِ ۱۲۰ میلی‌ثانیه است و درخواست‌های هم‌کلید (ویرایشگر و بوم) یک کار می‌شوند؛ بی OffscreenCanvas همان کار روی نخ اصلی.
  • پرونده: checkPhotoFile (components/photo/photo-file.ts) نوع (عکسِ شطرنجی؛ SVG نه) و سقفِ ۳۰ مگابایت را پیش از خواندن می‌سنجد؛ downscale با createImageBitmap(file, { imageOrientation: 'from-image' }) جهتِ EXIF را اعمال می‌کند (e2e/photo.spec.ts).
  • گوشه: قابِ عکس اشاره‌گر را می‌گیرد (setPointerCapture)؛ پایانِ کشیدن رها کردن یا lostpointercapture است، نه خروج از قاب.
  • مانع از کلیک روی عکس با clampObstacleCenter (components/photo/place.ts) درونِ دیوار می‌ماند.
  • برچسبِ نمونه‌های packages/geometry پیام است (src/labels.ts)، لبه رندرش می‌کند.

تطبیق دوربین با عکس (V7، پژوهشی)

Section titled “تطبیق دوربین با عکس (V7، پژوهشی)”

apps/web/src/lib/pose.ts از چهار گوشهٔ کالیبراسیون (photo.corners) و مستطیل مرجع (photo.reference) هموگرافی دیوار→عکس را با همان solveHomography ِ هسته می‌سازد (حل‌کنندهٔ دوم رفت) و با فرض فاصلهٔ کانونی، به وضعیت دوربین تجزیه می‌کند (H = K [r1 r2 t]، در قرارداد بینایی ماشین و بعد برگردان به دستگاه دیوار). photoCamera(wall, focal) در RenderBridge همان را به PerspectiveCamera سه‌بعدی در مختصات اتاق تبدیل می‌کند؛ رندر «سه‌رخ» با تیک «روی عکس اصلی، با دوربین عکس» با این دوربین و نسبت تصویرِ عکس گرفته و روی عکس اصلی (نه صاف‌شده) می‌نشیند.

⚠️ فاصلهٔ کانونی از عکس درنمی‌آید (EXIF نگه نمی‌داریم). حدس پیش‌فرض دوربین اصلی گوشی است (۲۶ میلی‌متر معادل: ضلع بلند × ۲۶ / ۳۶) و لغزندهٔ «فاصلهٔ کانونی» آن را از ۰٫۵ تا ۱٫۶ برابر می‌لغزاند. آزمون test/pose.test.ts با دوربین مصنوعی نشان می‌دهد بازیابی جا، جهت و زاویهٔ دید دقیق است (خطای بازتصویر زیر ۱۰⁻¹⁰ پیکسل، سقف پذیرفته ۳ پیکسل).

apps/web/e2e/visual.spec.ts از بوم سه‌بعدی آشپزخانهٔ نمونه در پوستهٔ تیره عکس می‌گیرد و با عکس مرجع می‌سنجد (maxDiffPixelRatio: 0.03). عکسِ مرجع فقط در تصویر رسمیِ Playwright در CI معتبر است و همان‌جا مقایسه می‌شود؛ نبودنش شکست است، و عکسِ تازه از کارِ دستیِ snapshots می‌آید (testing.md). تا نسل چهارم فقط عکسِ مک کامیت بود و آزمون بی عکسِ سکوی خودش می‌نوشتش و پاس می‌شد: هرگز مقایسه نکرده بود.