رفتن به محتوا

مدل داده

فهرست دقیق فیلدها همان پرونده‌های packages/core/src/types/ است — تیپ‌ها با کامنتِ چرایی. این سند نقشه و قراردادها را می‌گوید، نه تک‌تک فیلدها؛ سند تولیدی darz-format.md (فاز ۱، از اسکیمای zod) جای فهرست دقیق را می‌گیرد.

Project { version: 4, meta, settings, catalogRef?, catalogOverrides?, priceBookRef?, prices?, rooms[], views?, renders?, workshop?, variants?, stock[] }
meta { id, name, createdAt, updatedAt, notes?, tags?, migratedFrom? } // زمان: ISO با ساعت و منطقه (nowIso)
catalogRef { base: 'ir', version }
Room { id, name, ceilingHeight, finishes, walls[] }
finishes { wallColor, floor { kind, color }, backsplash?, soffit? }
Wall { id, name, length, height, origin, rotation, openings[], obstacles[], runs[], countertop?, accessories[], photo? }
Opening { id, kind: window|door, x, y, w, h }
Run { id, kind: base|wall|tall, startX, baseHeight, units[] }
Unit { id, number, kind, label?, presetId?, width, height, depth, construction, front, interior, materials, edging, hardware, cutouts?, appliance?, corner?, locked?, notes? }
Front { style, frame?, overlay, reveal, edgeReveal, opening, rows[] }
FrontOpening = handle { handleId } | pushToOpen { fittingId } | channel { edgeProfileId, midProfileId } | jPull | fingerGroove | none
Obstacle { id, kind, label, x, y, w, h, depth, mustStayAccessible }
Countertop { materialId, thickness, overhangFront, overhangEnds, backsplash?, seams[], cornerJoint? }
Accessory { id, kind: cornice|valance|endPanel|fillerToCeiling|ledStrip, run?, side?, height? }
PhotoRef { assetId, width, height, corners?, reference?, opacity }
Offcut { id, materialId, length, width, source }

فهرست کامل فیلدها با نوع و توضیح، تولیدشده از اسکیما: darz-format.md.

اسکیمای هر نسخه با zod در packages/core/src/schema/ است (v1.ts، v2.ts و v3.ts پروندهٔ همان نسخه را پیش از مهاجرت می‌سنجند؛ v4.ts با نوع Project هم‌تیپ است و تیپ‌اسکریپت این را چک می‌کند). loadProject پرونده را با اسکیمای همان نسخه می‌سنجد، MIGRATIONS[1] تا MIGRATIONS[3] (io/project-file.ts؛ گام‌ها migrateV1toV2 و migrateV2toV3 در io/migrations.ts، migrateV3toV4 در io/migrate-v4.ts) آن را گام‌به‌گام به ۴ می‌برند، و با اسکیمای آخر دوباره چک می‌کند. پروندهٔ خراب: DarzError با کد io/invalid و مسیر فیلد (rooms[0].walls[1].length). saveProject همیشه نسخهٔ فعلی را می‌نویسد.

Migration 3 → 4 (plan-v6 phase 2): format 3 is live — darzsaz.ir writes it since 2026-09-14 and the app validates every stored project at its own version before migrating — so the model changes of generation 6 are a new step, not an edit of 3 (the plan had folded them into 2 → 3 “before v3 is released”). Every front says how it opens (front.opening): hardware.handleId, else the dead front.handleId that three readers of format 3 fell back to, becomes handle; no handle at all is none. In format 4 front and hardware are strict objects: a handle id left behind is a migration that did not run, and dropping it would drop the handle from the cut list. The 2 → 3 step builds part ids with today’s builder, so its workshop remap reads the same project in format 4. Three format-3 files written by format-3 code (test/fixtures/*.v3.json) and the digest of what they derived (v3-digest.json: a hash per part over size, edging and machining, sheets, cuts, every hardware line) keep a live file’s cut list fixed (format-v3.test.ts, migration-v4.test.ts). Until the next deployment ships format 4, every model change of this generation joins this step.

مهاجرتِ ۲ ← ۳ یک گام است (تصمیمِ ۷ ِ پلن؛ تا انتشارِ نسخهٔ ۳ هر تغییرِ مدل به همین گام اضافه می‌شد): meta.thumbnail می‌رود، زمانِ meta ISO با ساعت (روزِ بی ساعت ← نیمه‌شبِ جهانیِ همان روز)، migratedFrom نسخهٔ پرونده، catalogRef پایهٔ ایران، و کلیدهای workshop روی شناسهٔ معنایی قطعه با نگاشتِ قطعیِ legacyPartIds (io/part-ids.ts). برچسبِ ساختاری (تصمیمِ ۷): یونیتی که برچسبش دقیقاً نامِ پیش‌فرضِ پیش‌تنظیمی از همان خانواده است (io/v2-preset-labels.json — داده، نه پیام، چون بیلدِ رابط متنِ پیام را برمی‌دارد) presetId می‌گیرد و برچسب نه؛ برچسبِ دیگر نامِ کاربر است و می‌ماند. نامِ دیوار و برچسبِ مانع دست نمی‌خورند. عکس و رندرِ درونِ سند هم در همین گام به assets می‌روند و سند assetId می‌گیرد («تصویر بیرون از سند»، پایین‌تر).

نامِ نمایشیِ یونیت فقط از unitLabel(u) (پیام) یا unitValue(u) (مقدارِ {unit} ِ قاعده‌ها) در catalog/preset-names.ts: برچسبِ کاربر، وگرنه پیامِ پیش‌تنظیم (unit.preset.*)، وگرنه «یونیت ۳». هیچ خواننده‌ای u.label را مستقیم نشان نمی‌دهد. کتابخانه (lib/unit-presets)، آشپزخانه‌های نمونه (fixtures/) و «این دیوار را پر کن» (layout/autofill.ts) فقط presetId می‌نویسند (UNIT_PRESET در units/names.ts)؛ شش نامِ فقط‌نمونه که کارت ندارند («ماشین ظرف‌شویی»، «دیواری چپ»…) با پیشوندِ sample- در همان PRESET_NAME اند. نامِ کارت همان پیام است مگر کارت نامِ دیگری داشته باشد (card: «دودرب» در تبِ دیواری، «دودرب قاب‌دار (کلاسیک)»). صاحبِ قطعه (PartName.owner/owners) هم پیام است، پس لیستِ برش و برچسبِ چاپی نامِ پیش‌تنظیم را به زبانِ خروجی دارند.

نامِ نمایشیِ دیوار، مانع و برش همین الگوست: wallLabel/wallValue و obstacleLabel (project/labels.tscutoutLabel/cutoutValue (layout/cutouts.ts) — نامِ کاربر (پیراسته)، وگرنه «دیوار n» به شمارهٔ اتاق (wall.new.name) یا نامِ نوع (OBSTACLE_NAME، cutout.kind.*). نامِ تهی یعنی «بی نام». رابط (lib/labels.ts)، برگه، قاعده‌ها و darzsaz units هیچ‌کدام wall.name/label را مستقیم نشان نمی‌دهند. نمونه‌ها (fixtures/) دیوار را بی نام و مانع و برش را بی برچسب می‌سازند (wallShell نام را تهی می‌گذارد)؛ مانعِ تازهٔ کاربر هم بی برچسب است و تعویضِ نوعش برچسب را تهی می‌کند. دیوارِ تازهٔ کاربر نامِ «دیوار n» را یک بار به زبانِ لحظه می‌گیرد (lib/walls.ts) تا حذفِ دیوارِ دیگر شماره‌اش را عوض نکند. پروندهٔ قدیم نامِ ذخیره‌شده‌اش را نگه می‌دارد.

پیکره‌های نسخهٔ ۱ و ۲ در packages/core/test/fixtures/*.v{1,2}.json منجمد شده‌اند (نسخهٔ ۲ با کدِ همان روز ساخته شد، با پیشرفتِ کارگاه و اثرِ هر شناسه در my-kitchen.v2.parts.json)؛ آزمون ادعا می‌کند پروژهٔ مهاجرت‌داده‌شده دقیقاً همان پیکرهٔ کد است، همان ۵۸ قطعه و ۵ ورق را می‌دهد، و هر تکهٔ تیک‌خوردهٔ کارگاه همان قطعهٔ فیزیکی است (migration-v3.test.ts).

شناسه‌ها با newId('u') (nanoid ده‌نویسه) ساخته می‌شوند — نه از ساعت و شمارنده که در دو تب برخورد می‌کرد. Unit.number شمارهٔ چاپی پایدار است: تازه = بیشترین + ۱ (nextUnitNumber)، حذف جابه‌جا نمی‌کند؛ صفر یعنی «هنوز شماره نگرفته» و numberUnits پرش می‌کند.

شناسهٔ قطعه معنایی است (PartBuilder، C11): unit:role:slot — پهلو با سمت (u3:side:left، کنجِ ال side:a)، بقیه با شمارهٔ درونِ همان نقش (u3:shelf:1، u3:back:0)، متعلقِ دیوار با شناسهٔ خودِ متعلق (acc:w1:cornice:a2). افزودنِ طبقه شناسهٔ پشت‌بند و درب را عوض نمی‌کند، پس QR ِ برچسب و پیشرفتِ کارگاه روی همان قطعه می‌مانند (parts-model.test.ts).

catalogOverrides ورق، نوار، ابعاد ورق، جنسِ صفحهٔ کابینت، یراق و پیش‌فرض‌های خودِ کاربر است و روی کاتالوگ پایهٔ ایران می‌نشیند (mergeCatalogData، ادغامِ عمیق: هر فهرستِ شناسه‌دار — یراق هم — با شناسه؛ شیء کلید به کلید تا هر عمق، catalog.md؛ همنام جایگزین، تازه اضافه). catalogFor(project) همان کاتالوگ را روی پایهٔ catalogRef.base (نبودنش: ایران) برای رابط و کارگر چیدمان می‌سازد، به ازای هر پایه و هر شیء overrides یک بار؛ پایهٔ ناشناخته catalog/unknown-base است — در loadProject هم، نه io/invalid ِ «کاتالوگِ کاربرِ پرونده ساخته نمی‌شود»؛ اعتبارسنجی همان اعتبارسنج کاتالوگ پایه است.

  • واحد همه‌چیز میلی‌متر است (MmroundMm گردکردن یکسان را نگه می‌دارد. اندازهٔ هر قطعه عددِ صحیح است (مرزِ generateParts)؛ مختصاتِ ماشین‌کاری روی گامِ ۰٫۵.
  • مختصات یونیت روی ردیف: x از لبهٔ چپ دیوار، y از کف. مختصات مانع هم همین است. بوم SVG وارونه است و svgYOf ترجمه می‌کند — نه مدل.
  • دیوار در اتاق: origin + rotation (درجه). دیوارِ بعدی از انتهای قبلی با ربع دور شروع می‌شود؛ cornerWall() (packages/geometry/src/room.ts) مبدأش را از طول حساب می‌کند تا گوشه بسته بماند. تنها فرمولِ جای دیوار wallFrame() در layout/wall-frame.ts است: place برای نقطه، direction برای بردار (فقط چرخش)، end برای انتها. گوشه‌ها از cornerPairs(room): هر جفت یک بار، انتهای a روی مبدأ b با زاویهٔ قائمه.
  • صفحهٔ کار قطعه به قطعه است (runPieces(wall, run, cat, room)، countertopsOf): یونیتِ قدی تکه را می‌شکند، رویه روی بلندترین یونیتِ تکه با ضخامتِ صفحهٔ خودِ دیوار. «رویهٔ صفحه اینجا کجاست» فقط از counterSurfaceAt(wall, x, cat, room) — قاعده‌ها، هندسه، بوم و برگه. گوشه (wallCorners، نسل ششم R10): ردیفی که تا گوشهٔ انتهای دیوار می‌رسد قطعه‌اش را تا خودِ گوشه می‌برد، بی پیش‌آمدگیِ سر؛ دیوارِ بعدی قطعه‌اش را از عمقِ همان صفحه شروع می‌کند (اتصالِ مستقیم؛ cornerJoint فقط می‌گوید سنگ‌کار چطور ببُرد) — تا نسل ششم هر دو سر ۲۰ پیش می‌آمدند و در گوشه ۴۰ دو بار شمرده می‌شد. بازوی ب ِ یونیتِ کنجِ ال روی دیوارِ بعدی هم صفحه می‌گیرد: اولین قطعهٔ آن دیوار تا عمقِ صفحهٔ قبلی عقب می‌رود، یا اگر ردیفش دورتر شروع شود قطعهٔ جدای …:arm.
  • دستگاه‌های مختصاتِ صفحه: Countertop.seams ِ ذخیره‌شده در مختصاتِ دیوار است. هر چیزِ مشتق‌شده روی یک قطعه — CountertopPiece.seams و CutoutRect.x/y — از لبهٔ چپ و جلوی همان قطعه است؛ CutoutRect.pieceId قطعه را می‌گوید و wallX همان لبه را در مختصاتِ دیوار می‌دهد برای قاعده‌هایی که با یونیتِ بالا می‌سنجند. سوراخ شیر kind: 'tapHole' است، نه برچسبی که با ترجمه عوض شود. CutoutRect.label هم پیام است، نه متن: نامی که کاربر روی برش گذاشته (text)، با نامِ دستگاهِ کاتالوگ اگر خودمان انتخابش کردیم («{cutout} — {appliance}»)؛ نامِ نوع از cutoutKindName(kind). دستگاهِ صحنه (FixtureInstance) با kind کشیده و پیدا می‌شود و label ش پیام است (I4).
  • ProjectSettings: kerf، trimAllowance، cuttingMethod: 'panelSaw' | 'cncNesting'، hasMiterCapability، sheetSizeIds[] (دست‌کم یکی)، currency: 'IRT' | 'IRR'، displayUnit، nestingQuality، cncToolDia، maxCutStages? (سقفِ مرحله‌های برشِ اره، R19؛ نبودنش یعنی defaults.cutting.maxCutStages ِ کاتالوگ — پروندهٔ نسل پنجم بی‌تغییر باز می‌شود).
  • origin و rotation دیوار در نسخهٔ ۲ اجباری‌اند؛ مهاجرت نبودنشان را «مبدأ» می‌نویسد.

ضخامت از جنس است، نه عدد: ضخامتِ بدنه carcassThickness(unit, cat) = ضخامتِ materials.carcass و ضخامتِ پشت‌بند backThickness(unit, cat) = ضخامتِ materials.back؛ carcassOf(unit, cat) و layoutFront(unit, cat) برای همین کاتالوگ می‌خواهند. construction.panelThickness، back.thickness، settings.defaultPanelThickness و defaults.dimensions.panelThickness رفتند: از پیش‌فرضِ ۱۶ پر می‌شدند و ویرایشِ جنس همگامشان نمی‌کرد، پس بدنهٔ ۱۸ کف و کلافِ ۴ میلی‌متر بلندتر می‌گرفت. پروندهٔ پیشین باز می‌شود و این کلیدها دور ریخته می‌شوند. فاصلهٔ جلوی بدنه از دیوار depthFromWall(unit, cat) است — عمقِ بدنه به‌علاوهٔ پشت‌بندِ روکار — و برخوردِ گوشه و صحنه از آن می‌خوانند؛ صفحهٔ کابینت عمداً نه (ورقِ بازار از دیوار اندازه می‌خورد).

Unit.construction: boxStyle (بدنه‌تمام‌قد یا کف‌تمام‌عرض)، topStyle (کلاف، کامل، بدون)، back (شیاری، روکار، بدون)، joinery، toeKick، پرچم‌های فارسی‌بر. Unit.front: سبک، نوع پوشش (Overlayopening، و rows[] از FrontCellها (door|glassDoor|sliding|drawer|open|falseFront|appliancePanel، عرض Mm | 'fill'، جهت لولا، جای دستگیره). front.opening (FrontOpening) says how the fronts are opened and is the one field the machining, the bill and the assistant read: handle (a handle item on every door and drawer, placed per front by FrontCell.handle; a handle without hole spacing is a rail handle bought by the metre), pushToOpen (a push latch, fittings of kind pushLatch, behind every front that opens; no holes), channel (aluminium handle channels in a base unit’s carcass: an L profile above the top row and a C between rows — shorter fronts, notched sides, the front rail set back; pipeline.md), jPull and fingerGroove (a grip routed from the back of each flat front along the edge a hand pulls, or into its face beside that edge — same size, routed by the metre) or none. A built unit (units/presets.ts) gets the catalogue’s default handle when it has a door or drawer and the caller names no opening; a lift door names the rail handle (defaults.ids.railHandle). نقشِ قطعهٔ هر خانه از frontRole(cell) است؛ appliancePanel قطعهٔ «نمای دستگاه» می‌شود — رگه ایستاده مثل درب، بی لانهٔ لولا و بی سوراخِ دستگیره.

A sliding cell is a door that runs on a track instead of swinging (parts/sliding.ts). Its row is one opening: every cell of the row slides or none does (front/sliding-mixed), the doors overlap by the track’s overlap where they pass — a negative gap in the same distribute that spaces the others — and the rails take topLoss off the top of the opening and bottomLoss off its floor. The track is front.sliding, else the catalogue’s own (defaults.ids.slidingTrack), and it is bought by the metre: a rail on top and one on the floor, the width of the opening each (trackMeters). A sliding door takes no hinge and no handle hole; its grain stands like a door’s. One door on a track is sliding-one-door: half the opening would never open.

A glassDoor cell is a door of aluminium frame and glass: it hangs on the same hinges, opens the same way and takes the same handle as a door (isDoorCell, the one predicate every check asks), but nothing is cut from a board for it — frontRole refuses it and parts/glass.ts measures the frame by the metre (the door’s perimeter, mitred) and the pane by the square metre (the door less faceWidth − rebate on each side). front.glass names the frame and the pane; without it the unit takes the catalogue’s own (defaults.ids.glassFrame, defaults.ids.glass). Until format 4 a whole unit could be style: 'glassFrame' and nothing was cut or bought for it (generation 6 / 1.4); migrateV3toV4 turns such a unit’s doors into glassDoor cells and its style into melamine for the panels still cut, so one unit can now have a glass door over a solid drawer. Unit.interior: طبقه‌ها و تیغه‌ها. materials/edging/hardware ارجاع به کاتالوگ با شناسه. edging.exposedSide نوارِ لبهٔ جلوی پهلوی انتهای ردیف است؛ نبودنش یعنی «مثل بدنه».

UnitKind دیگر openShelf ندارد و GrainMode فقط 'length' | 'none' است: هیچ‌کدام در هیچ نسخه‌ای ساخته نشده بود و چیدمان هر رگه‌ای جز none را «در امتدادِ طول» می‌گرفت.

پیش‌تنظیم‌ها (units/presets.ts) یونیت را با کاتالوگ می‌سازند و اعتبارسنجی می‌کنند؛ سازندهٔ رابط (UnitBuilder) همان توابع را صدا می‌زند.

قدی، کنج، دستگاه، متعلقات

Section titled “قدی، کنج، دستگاه، متعلقات”
  • قدی (kind: 'tall') در ردیف زمینی می‌نشیند و reservedIntervals(wall) بازه‌اش را برای ردیف دیواری رزرو می‌کند؛ صفحه دورش می‌شکند (counterSegments).
  • کنج (kind: 'baseCorner' | 'wallCorner', unit.corner): ال با legB روی دیوار مجاور (cornerLegB) و قطعات از parts/corner.ts؛ کور با blindWidth و نمای کور. corner.side می‌گوید کنج کدام سمت یونیت است. در ال (نسل ششم، R8) پاخور و پایه از ردِ پا می‌آیند نه از unit.width: پاخور دو دهانه (toeKickLength = faceA + faceB؛ مربعِ کنج جلو ندارد — برای ۹۰۰×۹۰۰×۵۶۰، ۶۸۰) و پایه شش گوشهٔ ال به‌علاوهٔ پایه‌های میانیِ بازوی بلند (cornerLegCount)؛ پشت‌بندِ شیاری با اندازهٔ شیار بریده و شیارش روی بدنه، کف و سقفِ هر بازو زده می‌شود؛ لنگه‌ها روکش را می‌بینند (cornerLeaf: تمام، نیم، توکار) و صحنه از همان می‌کشد.
  • دستگاهِ داخل یونیت (unit.appliance): فر، یخچال، مایکروویو، ظرف‌شویی — خریدنی و بی قطعه؛ برای قاعده‌ها و نمای دستگاهِ سه‌بعدی (applianceFixtures ِ packages/geometry). An oven or a microwave stands in a niche: the first open row of the front (parts/niche.ts, nicheOf). The built unit carries the fixed shelves that niche needs — the one the appliance stands on and the one that closes it from above, each only where the carcass is not already there — added by the model (withNicheShelves), never saved in the file, so they follow the row whenever it moves. They are screwed to the sides like any horizontal part (shelfJoints, and jointsOf counts them), while an adjustable shelf rests on pins and is not drilled. A preset asked for n shelves spreads them over the free spans beside the niche (shelfZones); until generation 6 they were spread over the whole inner height and the sample oven column had one inside the oven’s own opening (plan-v6 2.5).
  • جزیره (wall.kind: 'island', plan-v6 2.8): a run that stands free in the room, so the room sees the back of its units — Exposure.back (runExposure), and each unit carries a finished back (exposedBack): a panel of the front board over the whole back of the carcass, banded on all four edges, with the 3 mm backer that squares the box still behind it. Groundwork with no way in yet: the plan and the 3-D view draw an island like a wall (phase 5), and depthFromWall does not count the finished back’s thickness.
  • متعلقات دیوار (wall.accessories[]): پهلو، تاج، قرنیز، پرکن تا سقف، نوار نور؛ قطعه‌ها از parts/accessories.ts زیر یونیت ساختگی acc:<wall.id>، به جنس نمای ردیف.

داخل کابینت و درب قاب‌دار

Section titled “داخل کابینت و درب قاب‌دار”
  • interior.dividers[] تیغهٔ قائم است، x از رویهٔ داخلی بدنهٔ چپ؛ به بلندیِ فضای آزاد (clearHeight: از روی کف تا زیرِ سقف یا کلاف — کلاف هم یک ضخامت از بالا می‌گیرد؛ تا نسل ششم innerHeight بود و تیغهٔ زمینی روی کلاف می‌ایستاد، R3) و عمق طبقه بریده می‌شود، و اگر طبقهٔ تنظیم‌شونده هست دو ردیفِ سیستم ۳۲ ِ عبوری می‌گیرد. دربِ توکار هم زیرِ کلاف می‌نشیند.
  • تیغه‌ها داخل را به محفظه تقسیم می‌کنند (compartmentsOf(unit, geo)): هر عضوِ interior.shelves[] یک طبقه در هر محفظه است — طبقه از تیغه رد نمی‌شود (R2). شناسهٔ طبقهٔ محفظهٔ اول u3:shelf:0 می‌ماند و بقیه u3:shelf:0c1، u3:shelf:0c2؛ پروندهٔ بی‌تیغه هیچ شناسه‌ای عوض نمی‌کند و نگاشتِ نسخهٔ ۲ (legacyPartIds) نسخه‌های محفظه‌های دیگر را در شمارش نمی‌آورد. پینِ طبقه چهار عدد به ازای هر طبقه در هر محفظه.
  • interior.pullouts[] بیرون‌کش پشت درب: basket (سبد خریدنی، فقط یراق)، innerDrawer (جعبهٔ کشو بی‌نما، روی ریل یونیت یا slideId خودش)، tandemBox (بدنهٔ فلزی، فقط کف و پشت چوبی؛ ریل پیش‌فرض slide-tandembox-500). y از کف داخلی، height پیش‌فرض ۱۵۰.
  • ریل با type: 'tandemBox' در کشوهای نما هم همین کار را می‌کند: بدنه و جلو فلزی، عرض کف از boxWidthReduction. عمقِ کف length − bottomLengthReduction و بلندیِ پشتِ چوبیِ جعبهٔ فلزی backHeight ِ ریل است (نسل ششم، R9؛ تاندم‌باکس ۵۰: کفِ ۴۷۶ و پشتِ ۸۳)؛ ریلی که این فیلدها را ندارد مثل پیش بریده می‌شود. extension سهمِ بازشو است (۱ = تمام‌بازشو).
  • front.style: 'classic' یعنی درب قاب‌دار: هر نما پنج قطعه (frameStile×۲، frameRail×۲، doorPanel) با front.frame (پیش‌فرض قید ۷۰، شیار ۱۰) و materials.doorPanel (پیش‌فرض همان نما). قیدها به ترتیب لولا، آزاد، بالا، پایین ساخته می‌شوند و ماشین‌کاری به همین ترتیب تکیه دارد.
  • هر قطعهٔ نما frontIndex دارد: شمارهٔ نما در layoutFront. ماشین‌کاری با آن قطعه را به نمایش می‌رساند، نه با ترتیب.

settings.mutedRules[] = { ruleId, unitId?, reason } — کاربر از دستیار یک قاعده را با دلیل بی‌صدا می‌کند: برای یک یونیت، یا بی unitId برای مسئلهٔ پروژه‌ای همان قاعده (run-gap، extra-sheet، low-utilization). validateProject مسئلهٔ یونیت‌داری را که همهٔ یونیت‌هایش بی‌صدا شده‌اند، و مسئلهٔ بی‌یونیتی را که بی‌صدای بی‌یونیت دارد کنار می‌گذارد؛ بی‌صدای بی‌یونیت مسئلهٔ یونیت‌دار را نمی‌پوشاند. برگرداندن از فهرستِ «بی‌صداشده» ِ پایینِ پنل دستیار است.

  • validateProject(project, ctx, rules = RULES) هر قاعده را جدا اجرا می‌کند. قاعده‌ای که پرتاب کند، یا شدتی بسازد که در severity و meta.severities اعلام نکرده، یک مسئلهٔ rule-failed (یادآوری) با failure: { ruleId, error } می‌سازد — «بررسیِ X اجرا نشد» با «رونوشتِ گزارش» در دستیار. setRuleFailureHandler در test/setup.ts ِ core، report، web و cli پرتاب‌کننده می‌گذارد (و rules.test.ts ِ i18n هنگامِ ساختِ پیکره)، پس در این بسته‌ها هیچ شکستی در آزمون سبز نمی‌گذرد.
  • یونیتِ ناساختنی (هر DarzError ِ generateParts: بدنه، نما، ارجاعِ ناموجود در کاتالوگ) یک بار و روشن خطا می‌گیرد: unit-too-narrow برای عرض، unit-unbuildable برای بقیه. قاعده‌هایی که هندسه یا کاتالوگِ یونیت را می‌خوانند فقط buildable(w, cat) را می‌گردند — پیش‌شرطِ صریح، نه بلعیدن.

variants[] = { id, name, patch, createdAt } — گونه‌ها: تفاوت هر انتخاب با همین پروژه به‌صورت JSON Patch کمینه؛ priceBookRef = { id, vendor?, city?, capturedAt } می‌گوید قیمت‌ها از کدام دفتر ذخیره‌شده آمده‌اند؛ در catalog.md.

workshop = { counted, pieces, steps, updatedAt } — پیشرفت کارگاه (تحویل هر ردیف لیست برش، تکه‌های اسکن‌شده، گام‌های مونتاژ هر یونیت)؛ در workshop.md.

renders[] = { id, name, at, kind, width, assetId, cover? } — رندرهای نگه‌داشته‌شده در پروژه (حداکثر شش تا؛ cover یعنی جلد گزارش).

تصویر بیرون از سند (۹.۳، M1): عکسِ دیوار و رندر فقط assetId دارند — شناسهٔ محتوا (as-<۶۴ بیت درهم‌سازی>-<طولِ هگز>، io/assets.ts). بایت در .darz زیرِ assets/<id>.<png|jpg|webp> و در مرورگر در جدولِ assets (AssetRepo) است. سندِ ۳ با data URL رد می‌شود؛ پروندهٔ ۱ و ۲ که تصویر را درونِ سند داشت در همان گامِ ۲ ← ۳ تصویرش را بیرون می‌دهد (loadProjectWithAssets{ project, assets }) — شناسه از محتواست تا همان پرونده روی هر دستگاه و هر بار همان سند شود. هر جا پروژهٔ باز‌شده نوشته می‌شود، assets هم باید برود (وب: openStoredProject؛ خط فرمان: imageOf برای جلد). اندازه با آشپزخانهٔ نمونه، عکسِ ۱ و شش رندرِ ۲٫۵ مگابایتی: project.json ۲۱٫۳۵ ← ۰٫۰۲۰ مگابایت؛ ۱۰۰ قدمِ واگرد به‌صورتِ یک structured clone ۲۱٫۳۶ ← ۰٫۰۳۰ (بی رندر ۰٫۰۲۹ — مستقل از رندرها)؛ پیام‌های Worker ِ همان ۱۰۰ ویرایش ۲۱۳۴ ← ۰٫۶۹.

views[] = { id, name, pos: [x, y, z], target: [x, y, z] } — دوربین سه‌بعدی، متر در مختصات صحنه. از منوی «نماها» ِ نوارِ ابزارِ سه‌بعدی («ذخیرهٔ نمای فعلی…») ذخیره و اعمال می‌شود.

wall.countertop? (جنس از catalog.countertops، ضخامت، پیش‌آمدگی جلو و دو سر، درزها، درز گوشه) با پیش‌فرضِ countertopOf(wall, cat)؛ countertopsOf(project, cat) برای هر تکهٔ پیوستهٔ ردیف زمینی یک CountertopPiece می‌دهد (قدی تکه را می‌شکند). برش‌های صفحه (countertopCutouts)، سه‌بعدی، برگهٔ صفحه و خطوط صورت‌حساب همه از همین می‌آیند. جنس صفحه ورق بدنه نیست و در چیدمان ورق نمی‌رود.

قطعه و قرارداد نوار لبه

Section titled “قطعه و قرارداد نوار لبه”

Part { id, unitId, role, name, materialId, thickness, qty, finalSize, cutSize, cutSizeExact, grain, edging, machining[], notes?, frontIndex? }

نامِ قطعه — ساختار، نه متن

Section titled “نامِ قطعه — ساختار، نه متن”

برچسبِ قطعه ساخته نمی‌شود، رندر می‌شود (partLabel در core/parts/names.ts):

name کجا فارسی انگلیسی
{ owner: unitLabel(u), part: msg(side-left) } (یونیتِ پیش‌تنظیمِ «دودرب») هر قطعه؛ جای قطعه روی ورق (Placement) «دودرب — بدنهٔ چپ» “Two doors — Left side”
{ owners: ['الف', 'ب'], role: 'side' } (دو برچسبِ کاربر) ردیفِ ادغام‌شده از نام‌های گوناگون «بدنه — الف و ب» “Side — الف and ب”
  • owner نامِ یونیت است (unitLabel) — برچسبِ کاربر که ترجمه نمی‌شود و در رندر جهتش جداست، یا نامِ پیش‌تنظیم و «یونیت ۳» که پیامِ زبانِ خروجی‌اند — یا نامِ دیوار برای متعلقات (پهلو، تاج، قرنیز، پرکن)، دادهٔ کاربر. part خودِ قطعه در صاحبش است، یک پیام: «بدنهٔ چپ»، «درب ۱-۲»، «کف جعبهٔ کشو (۲)». شمارهٔ ردیف و خانهٔ نما و شمارهٔ کشو و طبقه count اند، پس رقمشان مالِ زبان است.
  • نقش و وصفش یک پیام‌اند، نه { role, qualifier } ِ جدا: «بدنهٔ چپ» کسره می‌خواهد و “Left side” ترتیبِ وارونه. نقش خودش روی Part.role هست؛ ادغام، سه‌بعدی و رنگِ نقشهٔ برش از همان می‌خوانند.
  • mergeParts نام‌ها را با partNameKey می‌سنجد، نه با متن: همه یکی ← همان نام؛ گوناگون ← { owners, role }. notes (هشدارِ برشکار) هم پیام است و کلیدِ ادغام شناسه و مقدارش را می‌بیند.
  • مصرف‌کننده‌ها فقط partLabel(p) را صدا می‌زنند (لیست برش، برچسبِ چاپی، نوار لبه، نقشهٔ برش، سوراخ‌کاری، CSV، کارگاه، خط فرمان، خطای DarzError با ref(partLabel(p)))؛ بازرس که صاحب را از پیش می‌داند، و راهنمای نقش‌های نقشهٔ برش، shortPartLabel(p).

چرا: تا نسل چهارم برچسب رشتهٔ فارسیِ ${unit.label} — بدنهٔ چپ بود و mergeParts نامِ یونیت را با بریدنِ همان رشته روی ' — ' پس می‌گرفت (I2): یونیتِ «زیر گاز — چپ» در ردیفِ ادغام‌شده «زیر گاز» می‌شد، ردیفِ دو پهلوی یک دیوار به‌جای نامِ دیوار «پهلوی چپ» می‌گرفت، بازرس نامِ یونیت را با replace از سرِ رشته برمی‌داشت، و رابط و برگهٔ انگلیسی برچسبِ فارسی چاپ می‌کردند. آزمون‌ها: core/test/part-names.test.ts (ساختار و ادغام) و i18n/test/part-labels.test.ts (هر نامِ هر پیکره به انگلیسی بی حرفِ فارسی؛ ۳۱ ردیفِ آشپزخانهٔ نمونه به فارسی).

دو تغییرِ دیدنی در فارسی، هر دو عمدی: «درب 1-1» ← «درب ۱-۱» (رقمِ زبان) و «دیواری چپ، دیواری راست» ← «دیواری چپ و دیواری راست» (Intl.ListFormat). برچسبِ متعلقات صاحب را اول می‌گذارد، مثل بقیه: «دیوار ۱ — تاج» (پیش‌تر «تاج — دیوار ۱»).

مهم‌ترین قرارداد پروژه در PartEdging:

  • alongLength[0..1]: دو لبه‌ای که به اندازهٔ طولاند و در دو سرِ عرض نشسته‌اند. نوار روی آن‌ها عرض را زیاد می‌کند، پس از عرض کم می‌شود.
  • alongWidth[0..1]: دو لبه‌ای که به اندازهٔ عرض‌اند و در دو سرِ طول نشسته‌اند. نوار روی آن‌ها از طول کم می‌شود.

cutSizeFor(final, edging, cat) این را پیاده می‌کند؛ آزمون واحد و آزمون ویژگی‌محور، جابه‌جا شدن این دو را می‌گیرند. جدول برش همیشه اندازهٔ برش را نشان می‌دهد — نوار از قبل کم شده.

grain: جهت رگه بر حسب نقش قطعه؛ در سه‌بعدی UV با همین قرارداد می‌چرخد تا رندر و برگهٔ برش یک چیز بگویند. machining[] را core/machining پر می‌کند (applyMachining در پایان generateParts): مختصات در دستگاه خودِ قطعه — x در امتداد طول (بدنه: از کف به بالا؛ درب: از پایین)، y در امتداد عرض (بدنه: از جلو به عقب؛ درب: از لبهٔ لولا). سوراخ edge روی لبه است. کنج ال ماشین‌کاریِ خودش را دارد (machining/corner.ts): صفحهٔ لولا روی پهلوی بازوی الف، لانه روی هر دو لنگه، اتصال روی هر دو پهلو. اتصال‌ها به نوعِ construction.joinery و با هندسهٔ defaults.joinery ِ کاتالوگ (catalog.md) زده می‌شوند: confirmat، minifixBolt + minifixCam، dowel، pocketHole. depth ِ سوراخِ روی رویه سوراخِ کور است و از ضخامتِ ورق بیشتر نمی‌شود (insidePart)؛ روی لبه در امتدادِ قطعه می‌رود.

CatalogData { version, materials, edgeBands, sheetSizes, countertops, hardware, defaults }createCatalog() شناسه‌ها را ایندکس و اعتبارسنجی می‌کند و خطای پیام‌دار با کد می‌دهد (DarzError، catalog/*). irCatalog() کاتالوگ ایران داخل بسته است. هر جا شناسه‌ای به کاتالوگ اشاره می‌کند (materialId، bandId، sheetSizeIds، شناسه‌های یراق) از همین راه حل می‌شود.

Derived { parts, merged, nesting, problems, issues, bom }pipeline.md. Issue { ruleId, severity: error|warning|info, category, message: Msg, unitIds, obstacleIds, fixes[], failure? } که هر fix { label: Msg, apply } است و apply تابعی خالص Project → Project که آزمون ادعا می‌کند واقعاً رفع می‌کند. پیام و برچسب شناسه و مقدارِ تیپ‌دارند، نه متن؛ هر لبه رندرشان می‌کند (i18n.md).