مدل داده
فهرست دقیق فیلدها همان پروندههای packages/core/src/types/ است — تیپها با
کامنتِ چرایی. این سند نقشه و قراردادها را میگوید، نه تکتک فیلدها؛ سند تولیدی
darz-format.md (فاز ۱، از اسکیمای zod) جای فهرست دقیق را میگیرد.
سلسلهمراتب (نسخهٔ ۴)
Section titled “سلسلهمراتب (نسخهٔ ۴)”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.
نسخه و مهاجرت
Section titled “نسخه و مهاجرت”اسکیمای هر نسخه با 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.ts)،
cutoutLabel/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).
شناسه و شماره
Section titled “شناسه و شماره”شناسهها با 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).
کاتالوگ خودِ پروژه
Section titled “کاتالوگ خودِ پروژه”catalogOverrides ورق، نوار، ابعاد ورق، جنسِ صفحهٔ کابینت، یراق و پیشفرضهای خودِ کاربر است و روی
کاتالوگ پایهٔ ایران مینشیند (mergeCatalogData، ادغامِ عمیق: هر فهرستِ شناسهدار — یراق هم — با شناسه؛
شیء کلید به کلید تا هر عمق، catalog.md؛
همنام جایگزین، تازه اضافه). catalogFor(project)
همان کاتالوگ را روی پایهٔ catalogRef.base (نبودنش: ایران) برای رابط و کارگر چیدمان میسازد، به ازای
هر پایه و هر شیء overrides یک بار؛ پایهٔ ناشناخته catalog/unknown-base است — در loadProject هم، نه
io/invalid ِ «کاتالوگِ کاربرِ پرونده ساخته نمیشود»؛
اعتبارسنجی همان اعتبارسنج کاتالوگ پایه است.
- واحد همهچیز میلیمتر است (
Mm)؛roundMmگردکردن یکسان را نگه میدارد. اندازهٔ هر قطعه عددِ صحیح است (مرزِ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: سبک، نوع پوشش (Overlay)، opening، و 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, andjointsOfcounts them), while an adjustable shelf rests on pins and is not drilled. A preset asked fornshelves 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), anddepthFromWalldoes 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. ماشینکاری با آن قطعه را به نمایش میرساند، نه با ترتیب.
قاعدههای بیصداشده
Section titled “قاعدههای بیصداشده”settings.mutedRules[] = { ruleId, unitId?, reason } — کاربر از دستیار یک قاعده را با دلیل
بیصدا میکند: برای یک یونیت، یا بی unitId برای مسئلهٔ پروژهای همان قاعده (run-gap،
extra-sheet، low-utilization). validateProject مسئلهٔ یونیتداری را که همهٔ یونیتهایش
بیصدا شدهاند، و مسئلهٔ بییونیتی را که بیصدای بییونیت دارد کنار میگذارد؛ بیصدای بییونیت
مسئلهٔ یونیتدار را نمیپوشاند. برگرداندن از فهرستِ «بیصداشده» ِ پایینِ پنل دستیار است.
موتورِ قاعده
Section titled “موتورِ قاعده”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)را میگردند — پیششرطِ صریح، نه بلعیدن.
نماهای ذخیرهشده
Section titled “نماهای ذخیرهشده”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] } — دوربین سهبعدی، متر در
مختصات صحنه. از منوی «نماها» ِ نوارِ ابزارِ سهبعدی («ذخیرهٔ نمای فعلی…») ذخیره و اعمال میشود.
صفحهٔ کابینت
Section titled “صفحهٔ کابینت”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)؛ روی لبه در امتدادِ قطعه میرود.
کاتالوگ
Section titled “کاتالوگ”CatalogData { version, materials, edgeBands, sheetSizes, countertops, hardware, defaults } — createCatalog()
شناسهها را ایندکس و اعتبارسنجی میکند و خطای پیامدار با کد میدهد (DarzError، catalog/*).
irCatalog() کاتالوگ ایران داخل بسته است. هر جا شناسهای به کاتالوگ اشاره میکند
(materialId، bandId، sheetSizeIds، شناسههای یراق) از همین راه حل میشود.
نتایج مشتق
Section titled “نتایج مشتق”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).