رفتن به محتوا

ویرایشگر دوبعدی نسل سوم

فاز ۴ پلن. بوم روبه‌روی دیوار در apps/web/src/components/WallCanvas.tsx و wall-canvas/؛ منطق تعامل در apps/web/src/editor/.

پوسته و چیدمان (فاز ۶ ِ نسل پنجم)

Section titled “پوسته و چیدمان (فاز ۶ ِ نسل پنجم)”
  • components/layout/WorkspaceLayout.tsx — کتابخانه (۱۶۸–۳۲۰، جمع‌شدنی با Enter روی جداکننده) · مرکز (کمینه ۴۸۰) · بازرس (۲۶۸–۴۸۰، آغاز ۳۲۰) روی SplitView ِ کیت. کمینه و بیشینه پیکسلی، سهمِ کشیده‌شده درصدی.
  • components/layout/CenterStage.tsx — سربرگِ مرکز (تب‌های دیوار، کلیدِ نما با ToggleButtonGroup؛ گروهِ «روی هم / کنار هم» و «جابه‌جا» فقط در «هر دو») و بدنه‌ای که نماها را می‌چیند. بوم و Scene3D با createPortal در میزبانِ ثابت رندر می‌شوند و خودِ میزبان به جای دیداری منتقل می‌شود؛ تعویضِ آرایش، ترتیب یا «سه‌بعدی ↔ هر دو» صحنه را از نو نمی‌سازد (test/center-stage.dom.test.tsx). فرمان‌ها: arrangementCommands در app/display-commands.ts (منوی نما و پالت؛ \ روی هم/کنار هم، Shift+\ جابه‌جا — matchesChord نشانهٔ Shift‌دار را با کلیدِ بی Shift ِ همان جا می‌سنجد). «کنار هم» وقتی صحنه از 2 × 240 + 8 پیکسل تنگ‌تر است «روی هم» چیده می‌شود (ResizeObserver روی صحنه)؛ انتخابِ کاربر در useUi نمی‌ماند که عوض شود و گزینهٔ «کنار هم» تا جا باز شود خاموش است. پیش‌فرضِ آرایش (defaultLayout ِ state/ui-layout.ts، یافتهٔ U9 ِ فاز ۵) محورِ بلندِ صحنه را نصف می‌کند: بومی که از بلندیِ صحنه پهن‌تر است کنار هم، وگرنه روی هم — نمای دیوار بلندتر از پهن است و نصف کردنِ ارتفاع در پنجرهٔ ۱۴۰۰×۹۰۰ دو نمای ۸۲۴×۳۷۰ می‌داد که نقشه در آن یک‌پنجمِ پنل بود.
  • وضعیت: state/ui.ts + state/ui-layout.tsdarzsaz.ui نسخهٔ ۲: { version: 2, layouts: { phone|tablet|desktop: { view, arrangement, swapped, center?, workspace? } }, rulers, grid, dims, tourSeen }. مهاجرتِ نسخهٔ ۱: split ِ پیکسلی ÷ ارتفاعِ مرکزِ همان پنجره ← سهمِ درصدی (۲۰ تا ۸۰)، فقط برای اندازهٔ جاری (test/ui-layout.test.ts). اندازهٔ صفحه با matchMedia (lib/useScreenClass.ts؛ ≤۷۶۸ گوشی، ≤۱۲۷۹ تبلت).
  • گوشی: یک نما در هر لحظه («هر دو» به دوبعدی برمی‌گردد)؛ BottomSheet با سه ایست (۱۲۰ پیکسل، ۵۰٪، ۹۲٪) که همان کتابخانه و همان بازرس را در تب‌های کیت میزبانی می‌کند؛ سربرگش دکمه است (فشردن = ایستِ بعدی)، کشیدن به نزدیک‌ترین ایست می‌نشیند.
  • دوربین (۶.۹): components/useFitFrame.ts — اندازهٔ اول از ResizeObserver (نه ۸۰۰×۳۰۰ ِ حدسی)، قاب فقط در اولین اندازه و «قاب»؛ تغییرِ اندازهٔ پنل و ویرایشِ پروژه چرخشِ کاربر را نگه می‌دارند.
  • پنجره‌ها: همه روی Dialog ِ کیت (۵.۵)؛ Overlay.tsx و قاعده‌های .sheetOverlay/.photoModal برداشته شدند. پنجرهٔ کاریِ بلند (عکس، رندر، چاپ) کلاسِ tallDialog ِ layout.module.css را می‌گیرد.
  • e2e: e2e/layout.spec.ts (پروژهٔ app) — کشیدن و بارگذاری دوباره، Enter روی جداکننده، آرایش با یک بومِ WebGL، برگهٔ گوشی در ۳۹۰×۸۴۴.

ماشین حالت تعامل — editor/interaction.ts

Section titled “ماشین حالت تعامل — editor/interaction.ts”

خالص و آزمون‌شده (test/interaction.test.ts): transition(mode, event) → { mode, effects }.

  • حالت‌ها: idle، pan، marquee (کادر انتخاب)، moveUnit، resizeBoundary، moveObstacle، resizeObstacle (چهار لبه، کمینهٔ ۲۰)، placeObstacle، dragFromLibrary، editNumber، moveRunHeight.
  • رویدادها: down (با هدف: زمینه، یونیت، مرز، مانع، لبهٔ مانع، خط ارتفاع ردیف، عدد عرض)، move (با Alt = چفت خاموش)، up، escape، pickObstacle، libraryDrag، dblclick.
  • اثرها (Effect) داده‌اند، نه فراخوانی: select، selectObstacle، beginDrag، liveBoundary، liveObstacle، liveObstacleBox، liveRunHeight، dropPreview، libraryPreview، commitMove، commitDrag، placeObstacle، dropLibrary، marquee، openNumber، startPan. جدول HANDLERS در wall-canvas/effects.ts هر اثر را روی فروشگاه اعمال می‌کند (آزمون: test/canvas-effects.dom.test.ts). کامپوننت فقط رویداد می‌فرستد (useCanvasInteraction).
  • کلیک بی‌حرکت روی یونیت انتخاب است؛ جابه‌جایی از DRAG_THRESHOLD_PX (۴ پیکسلِ صفحه، ۷.۱) به بعد شروع می‌شود — رویدادِ down مقیاسِ همان لحظه (pxPerMm از getScreenCTM) را می‌آورد و آستانه در Armed.slop میلی‌متر می‌شود. پیش‌تر ۱۲ میلی‌مترِ دیوار بود: ≈ ۱ پیکسل در «جا کردن» ِ دیوارِ ۱۲ متری و ≈ ۳۵ در بیشینهٔ بزرگ‌نمایی. مختصات همه در میلی‌متر دیوارند، y از کف.
  • کشیدن‌های زنده (مرز، مانع، لبهٔ مانع، خط ارتفاع؛ editor/live-drag.ts) هم تا آستانه فقط کلیک‌اند: beginDrag با گذشتن از آستانه یک بار و پیش از اولین اثرِ زنده، و commitDrag فقط اگر شروع شده باشد — کلیکِ بی‌کشیدن past.length را ثابت می‌گذارد (فاز ۷، ۷.۶).
  • یک مالک برای اشاره‌گر (۷.۲): down پرچمِ space دارد؛ Space ِ نگه‌داشته یا دکمهٔ میانی/راست روی هر هدف فقط pan + startPan است. useCanvasInteraction رویدادِ ماوس را روی بوم نگه می‌دارد (stopPropagation) و اشاره‌گر را برای کادر هم می‌گیرد، پس قابِ useViewport دوباره startPan نمی‌زند و خروج از SVG چیزی ثبت نمی‌کند (pointercancel = رها کردنِ کار). لمس روی زمینه هنوز به قاب می‌رسد (بزرگ‌نماییِ دو انگشتی).
  • Shift + کادر (marquee با additive) انتخاب را پاک نمی‌کند و یونیت‌های کادر را در رها کردن (commit) به انتخاب اضافه می‌کند؛ کادرِ ساده مثل قبل زنده جایگزین می‌کند.
  • snapBoundary: عرض استاندارد (catalog.defaults.standardWidths)، هم‌راستایی با مرز یونیت‌های ردیف دیگر (دیواری روی زمینی)، لبهٔ مانع با فاصلهٔ ایمن ۵۰؛ برد ۱۵ میلی‌متر (SNAP_TOLERANCE). هر چفت یک Guide دارد که بوم به شکل خط کهربایی نقطه‌چین و برچسب می‌کشد؛ عدد زنده در حباب کنار نشانگر. برچسب (GUIDE در snap.ts) و حباب (BUBBLE در effects.ts) پیام با مقدارِ تیپ‌دارِ mm() اند و wall-canvas/Guides.tsx در روکشِ فضای صفحه با render(i18n, …) می‌نویسدشان — زبان و رقمِ لحظه؛ تا موجِ ۳ متنِ فارسیِ سخت‌کد با رقمِ لاتین بود و در رابطِ انگلیسی فارسی می‌ماند.
  • snapHobUnderHood: مرکز یونیت گازدار به مرکز دودکش (یا یونیتِ «جای هود»)، برد ۳۰. هنگامِ جابه‌جاییِ یونیتِ گاز (wall-canvas/move.ts): مرکزِ یونیتِ کشیده‌شده همان فاصلهٔ لحظهٔ گرفتن را با اشاره‌گر دارد؛ اگر در برد است، جایی از ترتیب که مرکز آنجا به دودکش نزدیک‌ترین است انتخاب می‌شود و centreUnit همسایهٔ چپ (یا شروعِ ردیف) و راست را به یک اندازه جابه‌جا می‌کند تا گاز پهنایش را نگه دارد. همسایه بیرون از حدِ عرضش (widthLimits ِ هسته؛ قفل هیچ) کشیده نمی‌شود؛ اگر جا نیست راهنما هم نیست و جابه‌جایی ساده است. e2e: e2e/hob-snap.spec.ts.
  • حدِ عرض و جای x یک منبع دارند (۷٫۱۴): کشیدنِ مرز (resize.ts)، Alt+جهت‌نما، عددِ درجا، بازرس، کشو و سازنده عرض را از widthLimits/clampWidth ِ هسته می‌گیرند — ۱۵۰ تا ۱۲۰۰، قفل فقط عرضِ خودش، و یونیتی که از پیش بیرون از بازه است (الگوی دیوارِ بلند، پرکنِ ۱۰۰) تا عرضِ خودش، تا ویرایش هرگز برخلافِ جهتش نپرد. جای درج، مرزِ دستگیره، چفت، عددِ زنده، کادرِ انتخاب و بندانگشتی جای x را از layoutRun/runEdges می‌خوانند. test/math-sources.test.ts نسخهٔ تازه را در همهٔ بسته‌ها می‌گیرد.
  • snapRunHeight: ارتفاع نصب ردیف دیواری به ۶۰۰/۶۵۰/۷۰۰ میلی‌متر بالای صفحه.
  • Alt چفت را خاموش می‌کند (enabled: false).

خط‌کش (هر ۱۰۰ میلی‌متر) با کلید R، شبکه با G، اندازه‌گذاری (زنجیرهٔ عرض کنارِ هر ردیف — زیرِ ردیفِ زمینی، بالای ردیفِ دیواری —، ارتفاع ردیف کنار، فاصلهٔ مانع از گوشهٔ چپ) با D. وضعیت در state/ui.ts و localStorage؛ از منوی «نما» هم.

روکشِ فضای صفحه (۷٫۱)wall-canvas/ScreenOverlay.tsx یک SVG ِ جدا روی بوم: عددهای خط‌کش و اندازه و دستگیره‌های مرزِ یونیتِ انتخاب‌شده. جا از toScreen (همان ریاضیِ viewBox)، اندازه به پیکسل از screen-layout.ts: قلم ۱۱، دستگیرهٔ دیدنی ۸، ضربه ۲۴. گامِ عددِ خط‌کش با بزرگ‌نمایی (۱۰۰ تا ۱۰٬۰۰۰، فاصلهٔ کمینه ۵۶ پیکسل)؛ زنجیرهٔ اندازه اگر یک عدد جا نشود به جمعِ ردیف جمع می‌شود (data-collapsed). خط‌ها در لایهٔ دیوار با طرح بزرگ می‌شوند. لایه pointer-events: none است جز دستگیره‌ها؛ direction="ltr" چون textAnchor در راست‌به‌چپ وارونه می‌شود. خطِ راهنمای چفت (۲ پیکسل؛ راهنمای قائم سراسرِ بلندیِ قاب، راهنمای ارتفاع به طولِ دیوار) و حبابِ عددِ زنده هم اینجایند (data-guides، data-guide-label، data-bubble)؛ متنِ فارسی با unicodeBidi="plaintext". نشانِ مسئله (ScreenMarks.tsxIssueBadges: دایرهٔ ۱۶، ضربهٔ ۲۴، یک قطر بیرونِ لبهٔ یونیت و در سویی که زنجیرهٔ اندازهٔ آن ردیف نیست — ردیفِ دیواری زیر، زمینی بالا؛ یافتهٔ U9 ِ فاز ۵: روی گوشهٔ بالا-راست، نشان روی عددِ زنجیره می‌نشست)، نامِ مانع (ObstacleLabels: قلمِ ۱۱ و هالهٔ کاغذی؛ پیش‌تر در drawing.module.css به میلی‌مترِ نقشه بود و در نمای جاشدهٔ دیوارِ ۱۲ متری ≈ ۸ پیکسل می‌شد) و دستگیره‌های لبهٔ مانعِ انتخاب‌شده (ObstacleHandles: ضربهٔ ۲۴، دیدنیِ ۸؛ درونِ نوار هرگز بیش از یک‌چهارمِ مانع، تا پریزِ ۹ پیکسلی زیرِ نوارها گم نشود) هم اینجایند. حرکت و رها کردن روی قابِ بوم شنیده می‌شوند، نه SVG ِ دیوار: اشاره‌گرِ گرفته‌شده روی عنصرِ روکش به SVG ِ دیوار (خواهرش) نمی‌رسید.

نوارهای روی بوم بیرون حساب (۷.۶) — «افزودن مانع» (wall-canvas/ObstacleBar.tsx: یک دکمهٔ منوی کیت با دو بخشِ «مانع» و «بازشو»؛ هنگامِ گذاشتن راهنما و «لغو») و کنترلِ نما در پایینِ قاب شناورند. سهمِ هر نوار از محورِ خودش در fit به یک‌پنجم بسته است (U9 ِ فاز ۵): نوارِ ۵۶ پیکسلی از قابِ ۳۷۰ پیکسلیِ «هر دو» یک‌هفتم می‌گرفت، و حاشیهٔ درصدیِ fit کفِ ۱۲ پیکسل در هر سو دارد چون عددهای خط‌کش بیرونِ جعبهٔ محتوا کشیده می‌شوند. useBarInsets بلندیِ آن‌ها را از چیدمانِ واقعی می‌خوانَد ([data-view-controls]، [data-obstacle-bar]، ResizeObserver) و fit(content, frame, margin, insets) محتوا را در قابِ منهای نوارها جا می‌کند؛ settleView همان را می‌گیرد. پیش‌تر ردیفِ هشت‌دکمه‌ایِ نوارِ مانع در «هر دو» (بومِ ۸۲۴×۳۷۰ در ۱۴۰۰×۹۰۰) ۵۱۱×۷۴ پیکسل بود و روی یونیت‌های زمینی (پایینِ ۴۰۶ > بالای نوار ۳۸۸) می‌نشست و کلیکشان را می‌گرفت؛ حالا دکمه ۱۱۶×۳۲ و پایینِ یونیت‌ها ۳۶۸ < بالای نوارها ۴۳۰. بزرگ‌نمایی و جابه‌جاییِ دستی هنوز می‌توانند محتوا را زیرِ نوار ببرند.

نمای یونیت (UnitFace) خانه‌ها را از facadeCells ِ هندسه می‌کشد، یعنی layoutFront ِ هسته — با درز و درزِ لبه، همان که برشکار می‌بُرد (۷٫۱۴). آنچه روی نما دیده می‌شود از facadeMarks ِ همان بسته می‌آید و front.opening ِ یونیت را می‌خوانَد (نقشهٔ فاز ۲): میلهٔ دستگیره (ریلی: نوارِ لبهٔ بالا)، گشادیِ پروفیلِ دستگیرهٔ مخفی بالای ردیفِ اول و میانِ ردیف‌ها، و نوارِ فرزِ J یا شیارِ انگشتی — و برای نمای فشاری هیچ. پیش‌تر هر درب و کشو دستگیره داشت، هرچه بازش می‌کرد. پیش‌نمای سازنده همان نشان‌ها را می‌کشد (rect[data-mark]، test/facade.dom.test.tsx).

صحنهٔ تنبل (components/scene/SceneChunk.tsx، پیام در SceneStopped.tsx): شکستِ import() ِ تکهٔ سه‌بعدی ← «نمای سه‌بعدی بار نشد» با «دوباره» (یک lazy ِ تازه) و «برو به دوبعدی» (۷٫۱۰).

رجیستریِ فرمان و کلیدها (۷.۵) — app/registry.ts

Section titled “رجیستریِ فرمان و کلیدها (۷.۵) — app/registry.ts”

یک فهرست برای همهٔ فرمان‌ها: REGISTRY با scope (app پنجره، canvas بومِ متمرکز، unit یونیتِ متمرکز، scene برچسبِ نوارهای سه‌بعدی و F ِ «رفتن روی انتخاب» با تمرکز درونِ صحنه — components/scene/scene-keys.tsgroup ِ منو، برچسبِ /*i18n*/ و shortcut (+ keys ِ هم‌ارزِ نانوشته)؛ فرمان‌های منوی «نما» در app/registry-view.ts اند و درونِ همان REGISTRY پخش می‌شوند. همه از آن می‌خوانند: منوی بالا و پالت (commandOf در useCommandskeymap(i18n, scopes) برای صفحهٔ راهنما (سه جدول به لایه) و «راهنمای بوم» (app/CanvasHelp.tsx، دکمهٔ سربرگِ بوم به جای متنِ دائمی)، منوی راست‌کلیک و نوارِ شناورِ یونیت (برچسب + کلید)، canvas-keys.ts و unit-keys.ts (تطبیق با chordsOf + matchesChord). useShortcuts فقط لایهٔ app را روی پنجره می‌بندد؛ D/G/R در منوی «نما» دیده می‌شوند ولی مالِ بوم‌اند. آکوردِ Arrows یعنی هر چهار جهت‌نما؛ Shift برای حرف، رقم و کلیدِ نام‌دار سنجیده می‌شود، نه برای نشانه («?»، «+»).

لایه کلید
پنجره Ctrl+O/S/P/K/Z/Shift+Z/D، Delete (Backspace)، Escape، ۱–۴ نما، \ روی هم/کنار هم، Shift+\ جابه‌جا، ?
بوم + / − بزرگ‌نمایی، Shift+1 جا کردن، Shift+0 اندازهٔ واقعی (تصمیمِ ۴)، F انتخاب، D / G / R
یونیت جهت‌نما (Shift افزودن)، Ctrl+جهت‌نما جابه‌جایی، Alt+جهت‌نما عرض ±۱۰ (Alt+Shift ±۵۰)، L، Enter
صحنه F رفتن روی انتخاب (تمرکز درونِ نمای سه‌بعدی)

آزمون (test/command-registry.dom.test.tsx): نقشه از رجیستری، هیچ آکوردی دو فرمان ندارد در لایهٔ خودش و لایه‌های بیرونی، هر کلیدِ بوم کاری دارد، راهنما هر کلیدِ پنجره، بوم و یونیت را فهرست می‌کند. «۱» ِ تنها روی بوم دیگر preventDefault نمی‌کند، پس نما را عوض می‌کند.

جابه‌جایی و بزرگ‌نمایی (۷.۳، lib/viewport.ts): Ctrl/⌘ + چرخ و پینچِ صفحهٔ لمسی بزرگ‌نمایی (wheelIntent، ضریب پیوسته با سقفِ یک پله)؛ چرخِ تنها و دو انگشت روی صفحهٔ لمسی جابه‌جایی؛ روی نمایشگرِ لمسی یک انگشت جابه‌جایی و دو انگشت بزرگ‌نمایی + جابه‌جایی (touchStep). یک انگشت روی زمینهٔ بوم مثلِ Space است (کادرِ انتخاب نمی‌کشد). شنوندهٔ بومیِ غیرفعال‌نشده فقط پیش‌فرضِ مرورگر را می‌گیرد. بزرگ‌نماییِ کاربر با تغییرِ اندازهٔ دیوار یا قاب می‌ماند مگر محتوا از قاب بیرون برود (settleView، ۷.۷). پالت رقمِ فارسی و لاتین را یکی می‌بیند (paletteFilter).

جهت‌نما جهتِ روی صفحه است (V5): نقشهٔ دیوار در هیچ زبانی قرینه نمی‌شود، پس «راست» یونیتِ index+1 است، در فارسی هم.

میان‌برهای سراسری (lib/useShortcuts.ts): Delete (یا Backspace) برای هر انتخاب — همهٔ یونیت‌های selection، یا مانع/بازشوی selectedObstacleId — یک قدمِ واگرد (deleteSelected، hasDeletable برای فرمانِ «حذف انتخاب»)؛ «دوبار کلیک = حذف» ِ مانع برداشته شد. Escape فقط بالاترین لایه: انتخاب را وقتی پاک می‌کند که هدف درونِ dialog/alertdialog/menu/listbox نباشد و هنوز در صفحه باشد. کلیدِ بی Ctrl درونِ نقش‌های listbox، menu، menubar، slider، spinbutton، combobox، textbox، grid، tree (بر اساسِ نقش، نه تگ) و Alt ِ تنها هیچ میان‌بری نمی‌زنند. آزمون: test/canvas-input.dom.test.tsx.

selection (فهرست) و selectedUnitId (اصلی، آخرین) در فروشگاه؛ select(id, additive) برای Shift+کلیک، selectMany برای کادر انتخاب. بازرس اصلی را نشان می‌دهد؛ با بیش از یک یونیت properties/GroupEditor.tsx بالای بخش‌هایش می‌آید («ویرایش گروهی»، پایین‌تر).

خط آبی نقطه‌چین در baseHeight ردیف دیواری (RunHeightHandle) با کشیدن قائم عوض می‌شود؛ چفت به ۶۰۰/۶۵۰/۷۰۰ میلی‌متر بالای صفحه، عدد زنده «۶۵۰ بالای صفحه».

کارت کشو با ۶ پیکسل حرکت (startDrag در components/UnitDrawer.tsx) به «در دست» می‌رود (state/drag-source.ts) و سایه‌اش دنبال اشاره‌گر می‌آید؛ بوم (wall-canvas/library-drop.ts) روی پنجره گوش می‌دهد: داخل بوم ماشین حالت به dragFromLibrary می‌رود و libraryPreview خط درج را در ردیفِ همان y نشان می‌دهد (ردیفی که y در بلندی‌اش می‌افتد، وگرنه نزدیک‌ترین)؛ رها کردن dropLibrary است (همان lib/add-unit.ts که کلیک روی کارت هم استفاده می‌کند) و فقط اگر نوعِ آن ردیف با نوعِ یونیت یکی باشد درج می‌کند. کلیک بی‌حرکت مثل قبل می‌افزاید.

نوار شناور، منوی راست‌کلیک، عدد در جا (E6)

Section titled “نوار شناور، منوی راست‌کلیک، عدد در جا (E6)”
  • نوار شناور بالای یونیت اصلی (FloatingToolbar): تکثیر، قفل، آینه کردن درب، یادداشت (تمرکز روی کادر یادداشت بازرس)، حذف. جایش از ریاضی نما (toScreen) حساب می‌شود نه از DOM.
  • منوی راست‌کلیک (ContextMenu در @darzsaz/ui): همان فرمان‌ها (wall-canvas/unit-menu.ts، برچسب و کلید از رجیستری).
  • منوی دیوار (app/WallMenu.tsx، ⋯ کنارِ زبانه‌های دیوار): «پر کردنِ دیوار…» همیشه؛ دیوارِ پُر پیش از پنجرهٔ پیشنهادها ConfirmDialog می‌خواهد (۶.۸).
  • نوارِ سه‌بعدی (components/scene/SceneBar.tsx) روی OverlayBar ِ کیت: قاب، نزدیک/دور، چهار حالت، رندر — به ترتیبِ اولویت و سرریز به ⋯. «نمایش» منوی کیت با گروهِ چندانتخابی در نوارِ پایینِ صحنه (SceneTools) کنارِ برش، اندازه، روکش و لغزندهٔ بازشدن (۶.۴) — یک ردیفِ پیمایش‌پذیر، بی flex-wrap. رنگ‌گزینِ روکش فقط با رویدادِ بومیِ change ثبت می‌کند، نه در هر گامِ کشیدن (۷.۸). زیرِ ۳۰۰ پیکسل ارتفاعِ صحنه، SceneChrome تنها یک نوار می‌گذارد: ابزارها یک گزینهٔ همان نوارِ بالا می‌شوند و در Sheet ِ پایینی ستونی باز می‌شوند (U12 ِ فاز ۵ — دو نوارِ شناور یک‌پنجمِ پنلِ کوتاه را می‌گرفتند و نوارِ پایین روی کابینت‌ها می‌نشست).
  • «درها باز» و کلیک روی یک درب (نسل ششم، فاز ۳): در این حالت کلیک روی یک نما همان یکی را باز و بارِ دوم بسته می‌کند و انتخاب دست نمی‌خورد — در بقیهٔ حالت‌ها کلیک مثل همیشه یونیت را انتخاب می‌کند، وگرنه هر انتخابی یک درب را هم می‌چرخاند. لغزندهٔ «میزان باز شدن درها» مقدارِ همه است (scene.md، «حالتِ باز»).
  • دوبار کلیک روی عدد عرض: کادر عددی روی بوم (InlineNumber، foreignObject)؛ Enter/blur ثبت، Escape رها.

properties/GroupEditor.tsx وقتی بیش از یک یونیت انتخاب است: جنس بدنه، جنس نما، ارتفاع، عمق، قفل — روی همه با یک قدم واگرد؛ مقدار متفاوت «—».

  • کلیک روی مانع انتخابش می‌کند (selectedObstacleId) و بازرس مانع (properties/ObstacleInspector.tsx) می‌آید: نوع (از OBSTACLE_KINDS)، برچسب، x/y/w/h، عمق، «باید دسترسی داشته باشد»، حذف. چهار دستگیرهٔ لبه اندازه را می‌کشند.
  • پنجره و در بازشواند (wall.openings)، نه مانع: از بخشِ «بازشو» ِ منوی «افزودن مانع» (wall-canvas/ObstacleBar.tsx) انتخاب و با کلیک روی دیوار گذاشته می‌شوند (lib/openings.ts: در همیشه از کف)، OpeningLayer نشان می‌دهد، همان بازرس ویرایش می‌کند (فقط x/y/w/h — بی نوع، برچسب، عمق و دسترسی). قاعدهٔ هسته opening-blocked یونیتی را که روی بازشو بنشیند خطا می‌گیرد.

بازرسِ بخش‌بندی‌شده (E9، پلن ۶.۳)

Section titled “بازرسِ بخش‌بندی‌شده (E9، پلن ۶.۳)”

components/Properties.tsx سه شاخه دارد: بازرس مانع/بازشو، راهنمای خالی، و UnitInspector. هر سه در یک پوسته (inspector/InspectorShell.tsx): سربرگِ چسبان (نام + زیرنویسِ «ردیف · یونیت n · شمارهٔ چاپی») و منوی ⋯ ِ کیت — تکثیر، آینه کردن درب‌ها، قفل عرض (یا باز کردن قفل عرض)، یادداشت…، حذف یونیت (همان توابعِ نوارِ شناور؛ مانع فقط حذف).

بدنه بخش‌های بازشو است، نه تب (inspector/InspectorSection.tsx روی Disclosure/DisclosureGroup ِ کیت). بخشِ بسته خلاصهٔ یک‌خطی دارد (inspector/summaries.ts)؛ باز/بسته در localStorage با کلیدِ darzsaz.inspector می‌ماند (inspector/sections.ts؛ جدا از darzsaz.ui، پیش‌فرض فقط «اندازه»). پنلِ بسته در DOM هست (hidden="until-found")؛ جست‌وجوی مرورگر بازش می‌کند.

بخش چه چیزی خلاصهٔ بسته
اندازه نام، شمارهٔ چاپی، عرض/ارتفاع/عمق، قفل عرض، یادداشت عرض × ارتفاع × عمق
نما سبک، قاب کلاسیک، درزها، باز کردنِ نما (شش راه، OpeningFields)، چیدمان سریع، پیش‌نما، ردیف‌ها، و هر خانه: عرض، لولا، تقسیم/ادغام سبک · ردیف · خانه
داخل طبقه‌ها (نمای جانبی، پلهٔ ۳۲)، تیغه‌ها، بیرون‌کش‌ها طبقه · تیغه
ساختار سبک جعبه، کف، سقف/کلاف، پشت‌بند (جنس و شیار)، پاخور، اتصال، فارسی‌بر سقف · پشت‌بند
نوار لبه چهار لبه × نوار (بدنه، طبقه، نما، پهلوی نمایان) نوارِ لبهٔ جلوی بدنه
یراق لولا، ریل، پایه، جک (دستگیره در «نما» است) لولا
وسایل گاز/سینک/هود از نقشِ کاتالوگ (appliancesOf)، دستگاه توکار شمارِ وسیله
مسئله‌ها همان کارت‌های دستیار برای همین یونیت، با راه‌حل نشانِ شمار
قطعات این یونیت همهٔ قطعات؛ هر ردیف دکمه است — تمرکز، شناور و لمس همان قطعه را در سه‌بعدی روشن می‌کند (useUi.hoverPart)

Enter روی بوم اولین کنترلِ اولین بخشِ باز را تمرکز می‌دهد؛ «یادداشت…» بخشِ «اندازه» را اول باز می‌کند. راهش کنشِ تیپ‌دار است، نه رویدادِ document (۷.۱۴، V28): focusInspector('first' | 'width' | 'notes') ِ state/ports.ts درگاهی را صدا می‌زند که InspectorShell با ref (inspectorRef) گذاشته؛ همین فروشگاه openIssues (نشانِ بوم ← پنجرهٔ دستیار) و saveToFileRequested (اعلانِ ذخیره‌گاه) را به گیرنده‌های EditorPage (useEditorPorts) می‌رساند. بی گیرنده، کنش کاری نمی‌کند. چیدمانِ کنترل‌ها در inspector/fields.module.css است (موجِ ۳؛ پیش‌تر .propBody/.row3/.rowEdit/.cellEdit ِ سراسری در styles.css): ردیفِ سه‌کنترلی (row3) در ظرفِ زیرِ ۳۰۰ پیکسل دوستونه می‌شود (@container)؛ ردیفِ نما (rowEdit) ستونِ حذفِ auto دارد، نه 30px که دکمهٔ کیت با --hit ِ کارگاه (۴۸) از آن بیرون می‌زد؛ هر خانه کارتِ دوستونه است (cell: سربرگِ نام و تقسیم/ادغام، زیرش عرض، لولا، دستگیره) با role="group" به نامِ «خانهٔ ۱-۲». هر کنترل data-field="<مسیر مدل>" دارد؛ test/inspector-tabs.dom.test.tsx فهرستِ فیلدها را می‌گردد و e2e/inspector.spec.ts در ۲۶۸/۳۲۰/۴۸۰ × سه پوسته × دو زبان پیمایشِ افقی، تقاطع، در دید بودن و جا شدنِ هر خانهٔ ردیفِ نما (data-row-edit) را می‌سنجد. با انتخاب چندتایی، GroupEditor بالای بخش‌ها می‌آید.

دربِ شیشه‌ای (inspector/GlassFields.tsx، نقشهٔ فاز ۲): خانهٔ glassDoor در ویرایشگرِ ردیف‌ها و در شبکهٔ سازنده انتخاب می‌شود؛ تا یونیت یکی داشته باشد، دو فهرستِ «قابِ دربِ شیشه‌ای» و «شیشه» زیرِ بخشِ نما می‌آیند و هر دو شناسه را با هم می‌نویسند (یونیتِ بی‌انتخاب، شیشه و قابِ کاتالوگ را می‌گیرد — glassOf). بوم و پیش‌نما شیشه را ته‌رنگ می‌کشند (facadeMarks ِ glass) و صحنه شفاف (PartBox، DOOR_GLASS).

باز کردنِ نما (inspector/OpeningFields.tsx، نقشهٔ فاز ۲): شش راه در یک ToggleButtonGroup ِ قائم با آیکون — همه با هم دیده می‌شوند، چون این انتخاب نما و سوراخ‌کاری و فهرستِ خرید را با هم عوض می‌کند — و زیرش فقط فیلدهای همان راه (مدلِ دستگیره، جکِ فشاری، پروفیلِ L و C). راهی که هسته یونیت را برایش رد می‌کند اصلاً پیشنهاد نمی‌شود (kindsFor: دستگیرهٔ مخفی فقط زمینی، فرز فقط روی نمای صاف)، مگر همان که پرونده آورده. test/inspector-tabs.dom.test.tsx می‌سنجد که هر فیلدِ front.opening کنترل دارد، و e2e/opening.spec.ts مسیرِ کامل را: انتخابِ «فشاری، بی‌دستگیره» ← برگهٔ سوراخ‌کاری بی سوراخِ دستگیره.

پیش‌نمای نما (unit-builder/Preview.tsx، در سازنده و بخشِ «نما» ِ بازرس) خانه‌ها را از facadeCells می‌کشد — همان layoutFront ِ هسته که بوم و برگهٔ برش می‌بینند (۷.۱۴، V27)؛ جعبه‌ها به میلی‌متر و viewBox اندازهٔ یونیت است (test/facade.dom.test.tsx، ویژگی‌محور).

unit-builder/GridEditor.tsx: هر ردیف یک نوار از خانه‌ها؛ کلیک نوع خانه را می‌چرخاند، آیکونِ تقسیم و ادغام (lucide، جای «⫶» و «⊔»). خانه و دکمهٔ ریز Button ِ کیت‌اند و نوار با قاعده‌های تک‌کلاسیِ grid-editor.module.css به هم می‌چسبد — لایهٔ app ِ CSS در هر ترتیبِ بار بر kit می‌چربد (۶.۱۰)؛ .wrap .cell ِ دوکلاسیِ پیشین برداشته شد. RowDraft.cells نوع هر خانه را جدا نگه می‌دارد (rows.ts: splitCell، mergeCell، cycleCell)؛ فهرست ردیف‌ها همان چیز را از زاویهٔ دیگر ویرایش می‌کند و تغییر نوع/تعداد از آن، خانه‌ها را از نو می‌سازد.

  • روی هر یونیتی که مسئله دارد، نشان شمارنده (رنگ = بدترین شدت) گوشهٔ بالا-راست (ScreenMarksIssueBadges، به پیکسلِ صفحه)؛ کلیک، دستیار را با فیلتر همان یونیت باز می‌کند (openIssues ِ state/ports.ts).
  • IssuesPanel: فیلتر شدت، گروه‌بندی به یونیت (بی‌یونیت‌ها زیر «کل دیوار»)، «چرا؟» (issues/RuleWhy.tsx، همان در تبِ مسئله‌های بازرس) در زبانهٔ تازه به ردیفِ همان قاعده در «قاعده‌ها» ِ راهنما (#/help/rules/<id>، app/HelpRules.tsx، تنبل و پیش‌انباشته، پس آفلاین و در vite dev هم): لنگرِ rule-<id> تمرکز می‌گیرد، «چرا» از کاتالوگ به زبانِ رابط؛ فهرستِ سبکِ قاعده‌ها (lib/rule-index.ts، بی کدِ check) را scripts/rules-doc.mjs از RULES می‌سازد. همان صفحه از «کمک ▸ قاعده‌های دستیار» و دکمهٔ راهنما؛ «در مستندات» ِ هر ردیف سایتِ مستندات است (docs/[en/]reference/rules/#<id>، lib/rule-doc.ts؛ لنگرِ <a id> ِ هر ردیف از scripts/rules-doc.mjs، نه GitHub ِ خصوصی؛ سرویس‌ورکر ناوبریِ docs/ را به برنامه برنمی‌گرداند)، «پیامد؟» یک چیدمان سریع (۱۲۰ تلاش) روی پروژهٔ کنونی و اصلاح‌شده می‌زند و «+۱ ورق · −۱ مسئله» می‌گوید، «بی‌صدا برای این یونیت» (یا «برای این پروژه» برای مسئلهٔ بی‌یونیت) با دلیل → project.settings.mutedRules (هسته در validateProject فیلتر می‌کند)؛ پایینِ پنل فهرستِ بی‌صداشده‌ها با «برگرداندن». «بررسیِ X اجرا نشد» بی‌صدا نمی‌شود و «رونوشتِ گزارش» دارد (نسخه، قاعده، خطا). شناسهٔ قاعده فقط در حالت توسعه.
  • خروجی/چاپ با خطای ایمنی حل‌نشده تأیید می‌خواهد: useCommands پرسش را به SafetyConfirm (ConfirmDialog ِ کیت، «با این حال خروجی بگیر») می‌دهد، نه window.confirm.

دلتای زنده و ناحیهٔ زنده (E12، پلن ۶.۷)

Section titled “دلتای زنده و ناحیهٔ زنده (E12، پلن ۶.۷)”

state/derived.ts خلاصهٔ قبلی را نگه می‌دارد؛ summaryDelta ورق و هزینه را مقایسه می‌کند و StatusBar «این تغییر: …» را با رنگ می‌آورد. هزینه از Derived.bom.known (null اگر هیچ قلمی قیمت ندارد).

نوار زنده نیست. دو ناحیهٔ پنهان فقط: شمارِ مسئله (aria-live="polite") و دلتا (role="status")، هر دو با درنگِ ANNOUNCE_MS (۷۰۰) و خالی تا نخستین تغییر — یک محاسبهٔ چیدمان صفر اعلان است (test/status-live.dom.test.tsx).

components/Tour.tsx روی Popover ِ کیت با triggerRef به عنصرِ data-tour: جای پاپ‌اور از اندازهٔ واقعیِ هدف، تمرکز روی «بعدی»، Escape و کلیکِ بیرون «رد کردن». گامی که هدفش دیده نمی‌شود (گوشی) کنار می‌رود و با resize دوباره سنجیده می‌شود.

نمای «پلان» (کلید ۴): components/PlanCanvas.tsx روی wallSegments/roomRect (همان هندسهٔ اتاق سه‌بعدی). دیوارها، یونیت‌ها به عمق (دیواری کم‌عمق‌تر و نقطه‌چین)، بازشوها، موانع؛ کلیک دیوار انتخاب، پنل کنار: طول، زاویه، مبدأ x/z، «+ دیوار تازه (گوشه)». جزیره در مدل داده نیست.

e2e/perf.spec.ts: ۴۰ یونیت روی دیوار ۱۲ متری با window.__darzsazLoad (قلاب ابزار، همان مسیر loadProject، شناسهٔ پروژهٔ باز حفظ می‌شود)، کشیدن یک مرز با ۴۰ گام و ثبت فاصلهٔ فریم‌ها با rAF؛ p50 و p95 چاپ می‌شوند (قلاب فقط زیرِ navigator.webdriver؛ پروژهٔ perf، تنها). آستانه‌ها: کشیدنِ دوبعدی p50 < 34 ms؛ کشیدن در «هر دو» p50 < 26 و p95 < 64 ms؛ چرخاندنِ صحنه با ۴۰ یونیت p50 < 2200 و p95 < 5200 ms. سنجشِ فاز ۴ روی مک با WebGL نرم‌افزاری (فقط کشیدنِ دوبعدی): p50 ≈ ۱۷ ms (~۵۷ فریم)، p95 ≈ ۷۷ ms.

پنجرهٔ دوم (فاز ۸ ِ نسل پنجم)

Section titled “پنجرهٔ دوم (فاز ۸ ِ نسل پنجم)”

نمای سه‌بعدی (یا دوبعدی، یا پلان) در پنجره‌ای جدا، معمولاً روی مانیتورِ دوم؛ ویرایش همچنان در ویرایشگر است و پنجرهٔ پیرو فقط می‌بیند.

کاربر با کدام کلیک: سربرگِ مرکز ▸ دکمهٔ پنجره کنارِ هر نمای دیدنی («دوبعدی / سه‌بعدی / پلان در پنجرهٔ جدا»)، یا منوی نما ▸ «سه‌بعدی در پنجرهٔ جدا». تا پنجره باز است همان نما از چیدمان برداشته می‌شود و نشانِ «سه‌بعدی در پنجرهٔ دوم · برگرداندن» در سربرگ می‌آید؛ فشردنش پنجره را می‌بندد.

پرونده کار
components/layout/detached.ts نماهای بیرون (۸.۴): WindowProxy ِ window.open هر WATCH_MS (۵۰۰) برای closed پاییده می‌شود؛ stageView(view, detached) نمای روزِ مرکز («هر دو» منهای سه‌بعدی = دوبعدی)؛ انتخابِ useUi دست نمی‌خورد و آرایش برمی‌گردد
components/layout/DetachControls دکمهٔ هر نما در سربرگ (نه گوشهٔ پنل: OverlayBar ِ صحنه هر دو لبه را اندازه می‌گیرد) و نشانِ «برگرداندن»
lib/second-window.ts باز کردن: #/p/:id/view?pane=3d|2d|plan؛ مانیتورِ دیگر با getScreenDetails، وگرنه ۱۲۸۰×۸۰۰؛ WindowHandle یا null
lib/workspace-channel.ts پیام‌های zod، WorkspaceChannel (وصله ≤ ۱ در فریم، ضربانِ ۲ ثانیه)، WriterWatch (بسته پس از ۳ ثانیه)
lib/workspace-sync.ts نویسنده (serveViewers، با useWorkspaceWriter در ProjectSession تا قفل اینجاست) و پیرو (followWriter، useViewerLink)
app/ViewerPage.tsx پنجرهٔ پیرو: نوارِ نام، «تمام‌صفحه»، «برگرداندن به ویرایشگر»؛ «در انتظار» و «ویرایشگر بسته شد»
  • پیام‌ها: hello، snapshot (پروژه بی رندرهای ذخیره‌شده، دیوار، انتخاب، نمایش، پوسته، زبان)، patch (هر زیرمجموعهٔ ناتهی از همان‌ها، حداکثر یکی در هر فریم با پشتوانهٔ زمان‌سنجِ ۱۰۰ms برای پنجرهٔ پنهان یا پوشیده)، select، hover، saveView، bye، heartbeat. هر پیام پاکتِ { v, projectId, from, role } دارد و هر چه با اسکیما نخواند — نسخهٔ دیگرِ پروتکل، پروژهٔ بدشکل — به شنونده نمی‌رسد.
  • نویسنده فقط دارندهٔ قفلِ پروژه است و با گرفتنِ قفل «سلام» می‌کند؛ پیرو به نویسنده‌ای که عکسش را ندارد «سلام» می‌گوید و عکسِ کامل می‌گیرد — پس پس از «همین‌جا ادامه بده» ِ زبانهٔ دیگر هم همگام می‌ماند. بی پیروِ زنده هیچ وصله‌ای نمی‌فرستد (postMessage پروژه را حتی بی شنونده کپی می‌کند و عکسِ دیوار درونِ پروژه است)؛ پیرویی که ۳ ثانیه خاموش ماند و برگشت، عکسِ کامل می‌گیرد.
  • پیرو ویرایش نمی‌کند: فروشگاهش readOnly است، انتخاب و اشاره و «ذخیرهٔ نما» درخواست به نویسنده‌اند (ذخیرهٔ نما یک قدمِ واگردِ ویرایشگر می‌شود؛ Scene3D ِ onSaveView). تا عکس نرسیده «در انتظار ویرایشگر…» است، نه صحنهٔ پروژهٔ خالی؛ با رفتن از صفحه فروشگاه خالی می‌شود تا ویرایشگری که بعد در همان پنجره باز شود پروژه را از پایگاه بخواند.
  • بسته شدن: «خداحافظ» ِ pagehide همان لحظه، و سکوتِ ۳ ثانیه — مگر قفلِ پروژه هنوز گرفته باشد (کروم زمان‌سنجِ زبانهٔ پنهان را پس از ۵ دقیقه به دقیقه‌ای یک بار می‌رساند). «باز کردن ویرایشگر» همان پنجره را ویرایشگر می‌کند.
  • آزمون: test/workspace-channel.test.ts، test/second-window.test.ts، test/viewer-page.dom.test.tsx؛ e2e/second-window.spec.ts (دو صفحه: انتخاب و ویرایش در پیرو، صفر تراکنشِ نوشتنیِ IndexedDB، «بسته شد» در ≤ ۳ ثانیه؛ دکمهٔ پنل با یک زمینهٔ WebGL در دو پنجره و آرایشی که پس از بستن برمی‌گردد)؛ e2e/second-window.perf.spec.ts (درنگِ ویرایش تا پیرو، p95 ≤ ۱۰۰ms، در پروژهٔ perf؛ عددهای سنجیده در docs/journal/30-second-window.mdtest/center-stage.dom.test.tsx (پنلِ بیرون، «برگرداندن»، پنجرهٔ مسدود، «کنار هم» ِ تنگ).