Right Side Properties Pane
This page is the Design Document for the right-side icon bar, the object properties pane, and the first editable text-field UI element. To be referred by AI for coding as well as humans. It is a plan; sections marked as decisions are locked, defaults are changeable until first implementation.
The feature: a vertical icon bar occupies the right 8mm of the screen (below the top ribbon). It initially holds a single icon-only “Properties” button. Clicking it opens a 64mm wide properties pane immediately to the left of the icon bar. When an object is selected in the 3D scene, the pane shows Object Type, Object ID (64-bit, shown as 13-character Crockford Base32), and the object’s stored geometric fields, each in an editable text field. Typing a new numeric value and pressing Enter routes the change through the engineering thread, which updates the field and issues a geometry MODIFY command to the GPU copy thread.
Locked design decisions
- Raw stored fields. The pane shows exactly what the struct stores — Sphere:
Center X/Y/Z, Radius; Cylinder:P1 X/Y/Z, P2 X/Y/Z, Radius; etc. Edits map 1:1 to memory, no derived-field inverse mapping. Derived rows (center + orientation vector + length) are a documented future extension; the descriptor format reserves room for them. - Overlay, not viewport shrink. The icon bar and pane draw on top of the 3D scene like the
rest of the UI overlay. Scene rendering, GPU picking and placement math stay untouched; input
over the bar/pane is swallowed before it reaches the engineering thread’s scene handling.
Migrating to a shrunk viewport is a follow-up phase (touches
GetVisibleSceneViewportForTab, scene viewport/scissor, pick targets, 2D page rendering). - Compile-time descriptor tables describe which fields each object type exposes — the same
philosophy as the
AllUIControls[]giant array inUserInterface.h. One generic pane renderer + one generic apply function; supporting a new type = adding one table. Fields are addressed through typed get/set accessor function pointers, notoffsetof— the shape structs are not standard-layout (bothMETA_DATAand the derived shapes declare data members), sooffsetofon them is only conditionally supported by compilers. No virtual functions on the data structs (META_DATAlayout is frozen; a vtable pointer would break it). - UI/render thread owns text-edit state (focus, buffer, caret). Keystrokes never mutate engineering data. The engineering thread receives the committed value on Enter. The protocol additionally reserves a draft-value message so that future engineering-side validation (e.g. “length may not exceed 50m”) can run while the user types — see Draft validation (future). A minimal geometric validator ships with the MVP, not only in the future — see MVP validation.
- Number/text conversion (single format): text→float parsing uses fast_float
(
code-external/fast_floatgit submodule, header-only); float→text usesstd::to_charsshortest round-trip form. One format for both display and edit — the seeded string parses back to the identical bits, so clicking a field and pressing Enter without typing is a value-level no-op. No separate display vs edit formatting layer. - Object ID display: the 64-bit
memoryIDrenders as its fixed 13-character Crockford Base32 form viavishwakarma::crockford_base32::EncodeUInt64(CrockfordBase32.h, header-only).TryDecodeUInt64already tolerates i/l/o typos, ready for future ID search/entry fields.
UI geometry (all sizes in mm, converted per-monitor DPI like the top ribbon)
|<--- 64mm --->|<-8mm->|
------------+--------------+-------+
| PROPERTIES | I | <- both start below topUITotalHeightPx
3D | PANE | C |
VIEWPORT | (open/close) | O |
| | N |
| | S |
------------+--------------+-------+
- New constants in
UserInterface.h:UI_RIGHT_ICONBAR_WIDTH_MM = 8.0f,UI_RIGHT_PANE_WIDTH_MM = 64.0f. Vertical extent: fromtopRibbonLayout.topUITotalHeightPxto window bottom. - Icon bar buttons are square,
UI_ICON_SIZE_MMicons centered in the 8mm column, stacked from the top. Only one button for now: Properties. - Pane rows use the standard 2.5mm text (
UI_TEXT_HEIGHT_MM),UI_BUTTON_HEIGHT_MM-tall rows: label on the left ~24mm, value field on the right ~36mm, 2mm padding. - Colors reuse
UIColors(pane background =actionGroupBackground, field background = distinct light rectangle, focused field border =0xFF3399FFaccent already used by the data tree).
Components and ownership
1. Properties icon + pane visibility (render thread, per window)
SingleUIWindowgainsbool rightPaneOpen = false;(same class of UI-only state asactiveDropdownAction). Toggled directly in the immediate-mode hit test — no engineering round-trip, because it changes nothing but UI.- Properties icon: add
Commands::PROPERTIES_PANE = <random 10-digit id>toListOfCommands.h, aUITextIDentry (“Properties” — not “Settings”, which would imply app preferences), and anicon_<id>_properties_pane.svginwebsite/static/SVGIcons+ one line inSVGIconManifest.h(sliders/list glyph rather than a gear). The icon renders via the existingPushIcon/iconGlyphLookuppath — no new atlas work. - The click is not pushed to
g_actionQueue; it only flipsrightPaneOpen. (We still record usage statistics viaImprovementData::RecordRibbonActionfor parity with ribbon buttons.)
2. Property descriptor tables (compile time, shared)
New header PropertyPane.h (API-agnostic, like DataTreeView.h):
enum class PropertyFieldKind : uint8_t { Float32 /*, Float64, Int, Text, Derived... future*/ };
struct PropertyFieldDescriptor {
UITextID labelStringID; // e.g. UITextID::Radius, UITextID::CenterX
float (*get)(const META_DATA*); // typed accessors — see the layout note below
void (*set)(META_DATA*, float); // called by the engineering thread only
PropertyFieldKind kind; // MVP: Float32 only
uint8_t fieldIndex; // Stable per-type index, used in the edit protocol.
bool mustBePositive; // MVP validation hint (radii, diameters).
};
struct PropertyTypeDescriptor {
VishwakarmaStorage::ObjectType objectType;
const PropertyFieldDescriptor* fields;
uint8_t fieldCount;
// Optional cross-field rule (nullptr if none), e.g. PIPE inside < outside diameter,
// CYLINDER p1 != p2. The same function serves commits now and drafts later.
bool (*validate)(const META_DATA* object, uint8_t fieldIndex, float newValue);
};
extern const PropertyTypeDescriptor kPropertyTables[]; // Sphere, Cylinder, Cone, Torus,
// Ellipsoid, Pipe, FrustumOfCone ...
- Why accessors, not
offsetof: the shape structs are not standard-layout — both the baseMETA_DATAand the derived shapes declare data members, and C++ only guaranteesoffsetoffor standard-layout types (on others it is “conditionally supported”, i.e. a compiler-layout coupling rather than a language guarantee — a real concern with GCC/Clang ports planned). Each accessor is a tiny stateless lambda that decays to a function pointer:[](const META_DATA* o){ return static_cast<const SPHERE*>(o)->radius; }— fully portable, no casts at the call site, and type-checked at compile time. (VirtualGetProperties()methods remain rejected:META_DATAlayout is frozen and a vtable pointer would break it.) - Types whose geometry is a vertex list (
PYRAMID,CUBOID,PARALLELEPIPED,FRUSTUM_OF_PYRAMID) get an empty field table in the MVP — the pane shows Type + ID only. Editing individual vertices is a future extension (repeating row groups). - Reading a field for display:
fields[i].get(object). Writing (engineering thread only):fields[i].set(object, value).
3. Rendering the pane (render thread, in RenderUIOverlay)
Order of work inside the existing function, after the data-tree block:
- Draw the 8mm icon bar background + Settings button (hover/pressed tints like
PushInteractiveRect). - If
window.rightPaneOpen, draw the 64mm pane background. - Snapshot the selection: copy
tab.selection.selectedObjectIdsunderselectedMutex(the render thread already does this pattern for the highlight pass). - If exactly one object is selected, find its
StoredGeometryObject3Dintab.storageObjects3Dunder*tab.storageObjectsMutexand, still under that lock, copy out:objectType,memoryID, and the ≤ N floats named by the type’s descriptor table into a small stack array. Lock is held for microseconds; this is the same lock/copy discipline the data tree uses each frame.- Torn-read note: the engineering thread mutates fields without this mutex today. A torn
float read would only mis-display for one frame, but for correctness the plan makes the
engineering thread take
storageObjectsMutexfor the few stores of a property apply (see §5) — cheap and removes the race entirely.
- Torn-read note: the engineering thread mutates fields without this mutex today. A torn
float read would only mis-display for one frame, but for correctness the plan makes the
engineering thread take
- Render rows:
Object Type(viaVishwakarmaStorage::ObjectTypeDisplayName),Object IDas its 13-character Crockford Base32 form (vishwakarma::crockford_base32::EncodeUInt64fromCrockfordBase32.h— fixed width, no ambiguous I/L/O characters), then one label + text field per descriptor. Multi-selection or empty selection shows a static “0 / N objects selected” line (field editing for multi-select is out of scope). - Selection change (different
memoryIDthan last frame) cancels any in-progress edit.
4. The text field element (new reusable UI element)
State lives in a small per-window struct (SingleUIWindow.textEditState):
struct UITextEditState {
uint64_t focusedFieldKey = 0; // 0 = none. Key = hash(objectId, fieldIndex) — or the
// control's stable id for future ribbon/search reuse.
char32_t buffer[32] = {}; // Numeric entry never needs more.
uint8_t length = 0;
uint8_t caret = 0;
uint64_t editingObjectId = 0; // Guard: commit only to the object the edit started on.
};
Behaviour (immediate mode, evaluated every frame in the pane renderer):
- Click on a field → focus it and seed the buffer from the currently displayed value (caret at end, full-text implicitly “selected” is a nicety we skip in MVP).
- Number text conversion (single format): the displayed/seeded string is produced by
std::to_charsin its default shortest-round-trip form; parsing usesfast_float::from_chars(code-external/fast_floatgit submodule, header-only, faster and locale-independent vsstrtod). Round-trip means the seeded string parses back to the identical float bits — an untouched Enter can never silently alter the stored value. - While focused, the field renders the buffer instead of the live value, plus a blinking
caret (
GetTickCount64()/500 % 2, same idiom as the 2D text tool). - Character source:
UIInput.textInputThisFrame[32]already exists and is reset each frame by the render thread — but WndProc never fills it. Change inMain.cpp WM_CHAR: in addition to queueing to the tab, append the char intocurrentWindow->uiInput.textInputThisFrame(main thread and render thread already shareuiInputunder the existing snapshot-per-frame discipline). Accepted chars for numeric fields:0-9 . - + e E, Backspace (\b), Escape, Enter (\r). - Focus routing / suppression: while
focusedFieldKey != 0, the engineering thread must not interpret those keystrokes as shortcuts (‘P’ creates a pyramid today!). Addstd::atomic<uint64_t> uiKeyboardCaptureCount{0}(or a bool) onSingleUIWindow; WndProc checks it and simply does not queueWM_CHAR/WM_KEYDOWNprintable keys to the tab while a UI field has focus. Modifier tracking (SyncModifiersForWindow) continues unaffected. - Escape → revert buffer, drop focus. Click elsewhere → drop focus without committing (MVP; commit-on-blur is a later UX decision).
- Enter → parse with
fast_float::from_chars, then run the shared MVP validator (below) against the field snapshot. If parse or validation fails, flash the field red and keep focus. If it succeeds, push the commit action (below) and drop focus. The pane keeps rendering the old live value until the engineering thread’s apply lands (1–2 frames) — acceptable and honest: the field shows engineering truth, not UI hope.
This element is deliberately generic (UITextEditState + accepted-charset parameter) so the
ribbon SEARCH_BOX (ctrl.type == 3, currently a dead rectangle) can adopt it later.
5. Edit protocol: UI → engineering → copy thread
A committed edit must carry: tab, 64-bit object id, field index, and a double value —
UIActionEntry {uint32 id; uint64 p1; uint64 p2} cannot hold all of that.
Decision: extend UIActionEntry with one more uint64_t p3 rather than heap-allocating a
payload (the IMPORT_STD_FILE heap-pointer precedent works but is overkill for 3 words, and a
pointer would leak if a tab dies with queued actions). All existing PushUIAction call sites
keep working via the default argument.
// UserInterface.h
constexpr uint32_t kPropertyCommitUIAction = 0xE0000020u; // UI action id namespace
// Encode (render thread): p1 = (uint64_t(tabIndex) << 8) | fieldIndex
// p2 = objectMemoryId
// p3 = std::bit_cast<uint64_t>(double(value))
// Decode (main thread): tabIndex = uint32_t(p1 >> 8) — REJECT if >= MV_MAX_TABS;
// fieldIndex = uint8_t(p1 & 0xFF) — forwarded as-is; the
// engineering thread bounds-checks it against the resolved type's
// fieldCount (the object type is unknown until resolution there).
// UserInputProcessing.h
MODIFY_OBJECT_PROPERTY = 30022, // ACTION_TYPE. objectId = memoryID, x = fieldIndex,
// auxValue = std::bit_cast<uint64_t>(double value).
ACTION_DETAILS transport: add one uint64_t auxValue = 0; field (the struct is an internal
queue element, not persisted; growing it is free — today it carries no value payload beyond
objectId). PushSystemTodoToTab keeps its current shape and gains the trailing default:
PushSystemTodoToTab(DATASETTAB*, ACTION_TYPE, int x = 0, int y = 0, int delta = 0, uint64_t objectId = 0, uint64_t auxValue = 0) — all existing call sites compile unchanged.
Flow:
- Render thread (Enter pressed):
PushUIAction(kPropertyCommitUIAction, p1, p2, p3). - Main thread
ProcessPendingUIActions(): route toPushSystemTodoToTab(&allTabs[tabIndex], ACTION_TYPE::MODIFY_OBJECT_PROPERTY, fieldIndex, 0, 0, objectMemoryId, valueBits). - Engineering thread
todoCPUQueuehandlerModifyObjectProperty(myTab, objectId, fieldIndex, value). Lock discipline: the two mutexes are taken strictly one after the other, never nested — matchingAppendObjectToTabandRegisterGeneratedGeometryElement, which never holdstorageObjectsMutexandtoCopyThreadMutexsimultaneously. Nesting them would create a deadlock ordering hazard, and holdingstorageObjectsMutexthrough geometry generation would stall the render thread, which takes it every frame:- find the
StoredGeometryObject3DbymemoryId(linear scan, same as elsewhere); - look up the type’s
PropertyTypeDescriptor; bounds-checkfieldIndex; - re-run the MVP validator against live values (authoritative gate); on rejection, drop the commit — the UI thread pre-validated, so this only fires on races or bugs;
- under
*myTab->storageObjectsMutex:fields[i].set(object, value)andobject->dataVersion++— nothing else; release the lock; - with no lock held, regenerate:
GeometryData geo; GeometryForObject(objectType, object, geo);— this helper already exists inDataStorage.cppdoing exactly this switch over all 11 types; declare it in a header and reuse it, do not write a second copy; - push
{CommandToCopyThreadType::MODIFY, std::move(geo), object->memoryID, myTab->tabID, object->memoryIDParent}undertoCopyThreadMutexalone, thentoCopyThreadCV.notify_one().
- find the
- Copy thread: already handles MODIFY (in-place when it fits, grow/ADD path otherwise). Nothing to build here.
- Next frame the pane re-reads the stored field and displays the applied value.
MVP validation (commit-time, not future-only)
Raw-field editing can still poison GetGeometry() — a NaN center, a zero radius — so a
minimal geometric validator ships with Phase D. It is one pure function over the descriptor
tables, reused verbatim by the future draft channel:
- All fields: reject NaN and ±Inf (
std::isfinite). - Fields flagged
mustBePositive(radii, diameters, torus radii): reject values<= 0. - Per-type cross-field rules via
PropertyTypeDescriptor::validate:PIPEinside diameter < outside diameter;CYLINDER/PIPE/FRUSTUM_OF_CONEaxis end points must not coincide after the edit;TORUSminor radius < major radius. - The UI thread runs the same checks before pushing the commit (it holds the field snapshot), so rejection feedback is immediate — red flash, keep focus. The engineering thread re-runs them against live values as the authoritative gate and silently drops failures. One function, two call sites, no divergence.
6. Input swallowing over the bar/pane (fixes click-through)
Today WndProc queues every mouse event to the tab, and the engineering thread issues a GPU pick for any left-click not over the top UI. With an overlay pane, clicks on the pane would also select/deselect objects underneath — the classic click-through bug. Guard on both sides of the queue:
SingleUIWindowgainsstd::atomic<uint32_t> rightOverlayWidthPx{0}— written once per frame by the render thread (8mm, or 72mm when open, DPI-scaled). Both guards below read it.- WndProc side (primary):
Main.cppalready suppresses tab-queueing for UI regions viaIsClientPointOverTopRibbonandIsClientPointOverDataTree. Add the siblingIsClientPointOverRightOverlay(window, pt)— x withinrightOverlayWidthPxof the client right edge, y below the ribbon — and skip queueing clicks and wheel events to the tab when it hits, exactly like the existing data-tree paths. - Engineering-thread side (backup): in the
LBUTTONDOWN/ placement / 2D handlers, ignore scene interaction wheninput.x >= viewportWidth - rightOverlayWidthPx(helperIsOverRightOverlay(tab, x)that resolves the window the same wayGetVisibleSceneViewportForTabdoes). This covers events already sitting in the queue when the pane opens, and any future non-WndProc input source. Mouse wheel over the pane likewise skips the camera-zoom path (the pane will want wheel-scrolling once content exceeds the window height — MVP just swallows it).
Draft validation (future — designed for, not built now)
Per the locked decision, edit state stays on the UI thread, but drafts can flow to engineering:
- On each buffer change (or throttled to ~10 Hz), the render thread pushes
kPropertyDraftUIActionwith the same payload as a commit. The engineering thread runs the validation half ofModifyObjectPropertyonly (no store, no MODIFY command) and writes the verdict into a small per-tabstd::atomic<uint64_t> propertyDraftVerdict(packed: field key + ok/fail + reason enum). The render thread reads it next frame and tints the field red/normal. Enter then sends the normal commit, which re-validates authoritatively. - Because drafts and commits travel the same route and are validated by the same function, there is no divergence between “what looked valid while typing” and “what commit accepts”.
Phased build order
| Phase | Deliverable | Verify |
|---|---|---|
| A | Icon bar + Properties SVG + rightPaneOpen toggle, empty pane, both input guards of §6 |
Bar renders at 8mm on 96/144 DPI; icon toggles pane; clicks/wheel over pane no longer pick/deselect/zoom the 3D scene |
| B | Descriptor tables (accessors) + read-only pane rows | Select sphere/cylinder in 3D → Type, Base32 ID and correct live values appear; values track Randomize()d objects; multi/empty selection shows count line |
| C | Text field element + WndProc WM_CHAR routing + keyboard capture flag |
Click field → type → caret/backspace/escape behave; ‘P’ no longer spawns pyramids while typing; seeded string is to_chars round-trip form |
| D | Commit path (p3, MODIFY_OBJECT_PROPERTY, validator, ModifyObjectProperty, MODIFY push) |
Edit sphere radius + Enter → geometry visibly changes; pane re-reads applied value; NaN, -1 radius, inf rejected with red flash; untouched Enter is a value no-op; undo/redo explicitly out of scope |
Explicit non-goals of this iteration
- Multi-select editing, vertex-list editing (CUBOID/PYRAMID families), derived fields
(orientation vector / length), units display & conversion, pane scrolling, commit-on-blur,
undo/redo integration, persistence of
rightPaneOpen, localization of new labels beyond registeringUITextIDs, and the viewport-shrink layout. Each is listed so it is a conscious cut, not an oversight.
Open questions (defaults chosen, change before implementation if desired)
- Where the Properties icon sits when more right-bar icons arrive: stacked from top, Properties stays first.
- Pane state per window vs per tab: per window (matches
activeDropdownAction); switching tabs keeps the pane open and simply shows the new tab’s selection. - Friendlier display formatting (fixed decimals per unit category) may come later — if it
does, the edit seed must stay the
to_charsround-trip string; only the at-rest display may be shortened.
Next iteration (planned): proto-numbered property descriptors
Everything below this line is the design for the NEXT property-pane iteration. Decisions are
locked; implementation has not started. It streamlines the descriptor system above so that
every property carries a numeric ID equal to its field number in the type’s .proto file,
a full property type system, and an optional numeric range — so a non-CS developer can add an
object type or property by editing one file section and following one mechanical rule.
Architecture fact this design builds on (verified): the in-memory object definitions are the
C++ structs (डेटा-सामान्य-3D.h, डेटा-पाइप.h, डेटा-संरचना.h) — not the protos. Protos are
storage-only: DataStorage.cpp hand-copies struct fields into generated protobuf-lite messages
and stores the blob in SQLite. Today proto field numbers appear nowhere in C++; the struct↔proto
correspondence exists only in the statement order of the hand-written EncodeXxx/DecodeXxx
functions, with nothing enforcing it. This iteration makes proto field numbers the stable,
compile-time-enforced property IDs.
Locked decisions of this iteration
- Unroll composite proto fields in the .proto files. Singular
Point3F/Color4Fmessage fields become flat scalars, each with its own field number → property ID ↔ proto number is exactly 1:1. This is a BREAKING storage change, accepted pre-release: no migration; old.yyyfiles fail to load with a clear error (a load gate turns silent mis-decode into an explicit message). - Property ID = proto field number,
uint16_tin the descriptor, enforced bystatic_assertagainst the protoc-generatedk<Field>FieldNumberconstants. - Property tables move next to their structs in the data headers, as
inline constexprarrays — struct + properties visible in one place.PropertyPane.cppshrinks to the registry, lookup, validation, and the static_assert block. - Full
PropertyKindenum now, phased activation:Bool, Uint, Int, Float, Double, Utf8String. This iteration keeps only Float editable end-to-end (pixel-identical UX); the enum and accessor signatures are final so tables never churn when later kinds activate. - Numeric range replaces
mustBePositive: inclusiveminValue/maxValuedoubles per field (radii use a “smallest positive” constant). Cross-field validators stay. - Labels stay
UITextID(localized viaUserInterfaceTranslation.csv+UserInterfaceTranslationCompiler.py). - The edit protocol keys on propertyID (stable identity end-to-end) instead of the array
index; the
fieldIndexdescriptor member is deleted.
Proto unroll rule and new field layouts
Mechanical rule, applied to all 3D/fitting protos in one pass (a single storage break): keep
each message’s declaration order; Point3F foo → float foo_x, foo_y, foo_z (3 numbers);
Color4F bar → float bar_r, bar_g, bar_b, bar_a (4 numbers); plain scalars keep 1 number;
repeated fields stay composite (vertex lists); renumber sequentially from 1. Colors unroll
too — one uniform rule; Point3F/Color4F in DataStorage_Common3D.proto remain only for
repeated use. Each proto gains the comment: “Field numbers are permanent property IDs:
never renumber or reuse them; only append new fields.”
New layouts (field = number):
- Sphere: center_x/y/z = 1-3, radius = 4, color_r/g/b/a = 5-8
- Cylinder: p1 1-3, p2 4-6, radius = 7, color_base 8-11, color_top 12-15, color_incline 16-19
- Cone: apex 1-3, base_center 4-6, radius = 7, color_base 8-11, color_incline 12-15
- Torus: center 1-3, major_radius = 4, minor_radius = 5, color 6-9
- Ellipsoid: center 1-3, radius_x = 4, radius_y = 5, radius_z = 6, color 7-10
- Pipe: center1 1-3, center2 4-6, outside_diameter = 7, inside_diameter = 8, color_outer 9-12, color_inner 13-16, color_cap 17-20
- FrustumOfCone: bottom_center 1-3, top_center 4-6, bottom_radius = 7, top_radius = 8, color_base 9-12, color_top 13-16, color_incline 17-20
- Elbow: center 1-3, bend_radius = 4, outside_diameter = 5, inside_diameter = 6, sweep_angle_radians = 7, color_outer 8-11, color_inner 12-15, color_cap 16-19
- Tee: center1 1-3, center2 4-6, main_outside_diameter = 7, main_inside_diameter = 8, branch_angle_degrees = 9, branch_length = 10, branch_outside_diameter = 11, branch_inside_diameter = 12, color_outer 13-16, color_inner 17-20, color_cap 21-24
- Flange: center1 1-3, center2 4-6, flange_outer_diameter = 7, bore_diameter = 8, raised_face_diameter = 9, raised_face_projection = 10, color_face 11-14, color_rim 15-18, color_bore 19-22
- LineMember: point1 1-3, point2 4-6, profile_id = 7 (uint64), color_main 8-11, color_inner 12-15, color_cap 16-19, user_parameter1 = 20, user_parameter2 = 21
- Cuboid: vertices = 1 (repeated, unchanged), color_r/g/b/a = 2-5
- Parallelepiped: vertices = 1, color 2-5
- FrustumOfPyramid: vertices = 1, color_base 2-5, color_top 6-9, color_incline 10-13
- Pyramid (all repeated), Folder/Page2D/Scene3D (no composites), and all 2D protos: unchanged.
Protos whose singular composites disappear drop the now-unused DataStorage_Common3D.proto
import (11 files); the three vertex-list types above keep it.
The new descriptor
enum class PropertyKind : uint8_t { Bool, Uint, Int, Float, Double, Utf8String };
// This iteration renders/edits only Float; the enum is complete so tables never churn.
inline constexpr double kNoMinLimit = -std::numeric_limits<double>::infinity();
inline constexpr double kNoMaxLimit = std::numeric_limits<double>::infinity();
inline constexpr double kMinPositive = FLT_MIN; // "> 0" for float-backed fields.
struct PropertyFieldDescriptor {
uint16_t propertyID; // == field number in DataStorage_<TYPE>.proto. Stable forever.
UITextID labelStringID;
PropertyKind kind;
double (*get)(const META_DATA*); // numeric kinds; set() casts to the stored type
void (*set)(META_DATA*, double);// engineering thread only
double minValue; // inclusive range (replaces mustBePositive)
double maxValue;
};
- Accessors become
double-based: one migration covers Bool/Uint/Int/Float/Double later (Utf8Stringgets its own accessor pair as trailing members in its phase). Documented limit: uint64 values above 2^53 lose precision through a double — revisit if a real ID field ever exceeds it (profile_idcatalog ids do not). fieldIndexis deleted — array position serves the snapshot arrays; the protocol usespropertyID.PropertyTypeDescriptorkeeps its shape;validateCrossFieldbecomes(const double* values, uint8_t count, uint8_t editIndex, double newValue).ValidatePropertyEdit:std::isfinite(v) && minValue <= v && v <= maxValue+ cross-field rule.CrossLineMemberdrops its>= 0part (now per-fieldminValue = 0.0).UserInterfaceTranslationCompiled.his enum-only (~350 lines) — safe to reach the data headers viaPropertyPane.h, which keeps forward-declaringMETA_DATA(no include cycle).
Tables live next to their structs
Declared inline constexpr immediately below each struct (C++20 is already the project
standard; captureless-lambda → function-pointer conversion is constexpr). Example, in
डेटा-सामान्य-3D.h below SPHERE:
// Properties pane declaration. propertyID = field number in DataStorage_SPHERE.proto
// (enforced by static_asserts in PropertyPane.cpp).
inline constexpr PropertyFieldDescriptor kSphereProperties[] = {
{ 1, UITextID::PropCenterX, PropertyKind::Float,
[](const META_DATA* o) -> double { return static_cast<const SPHERE*>(o)->center.x; },
[](META_DATA* o, double v) { static_cast<SPHERE*>(o)->center.x = static_cast<float>(v); },
kNoMinLimit, kNoMaxLimit },
// ... center_y = 2, center_z = 3 ...
{ 4, UITextID::PropRadius, PropertyKind::Float, /* get/set */, kMinPositive, kNoMaxLimit },
};
Cross-field validators (CrossTwoPoints, CrossTorus, CrossPipe, CrossLineMember) move
as inline functions next to the tables they serve, so a type’s whole property story sits in
one file section. Affected headers: डेटा-सामान्य-3D.h (Sphere, Cylinder, Cone, Torus,
Ellipsoid, Pipe, FrustumOfCone), डेटा-संरचना.h (LineMember). PropertyPane.cpp retains only:
kPropertyTables[] registry (one line per type), FindPropertyTable, ValidatePropertyEdit,
and the static_assert block.
Compile-time ID enforcement
PropertyPane.cpp includes the generated pb headers it needs (bare-name include —
$(IntDir)GeneratedProtobuf is already on the include path, exactly as DataStorage.cpp
does) and asserts every entry:
static_assert(kSphereProperties[0].propertyID == pb::Sphere::kCenterXFieldNumber);
static_assert(kSphereProperties[3].propertyID == pb::Sphere::kRadiusFieldNumber);
Contingency: if MSVC rejects constexpr-lambda tables, fall back to inline const tables plus
a parallel constexpr uint16_t kSpherePropertyIDs[] used by the asserts.
Edit protocol switches to propertyID
UserInterface.cppcommit:p1 = (uint64_t(tabIndex) << 16) | propertyID(was<< 8 | fieldIndex);focusedFieldKeyhashes propertyID.Main.cpprouting unpacks theuint16and forwards it in the sameMODIFY_OBJECT_PROPERTYslot.विश्वकर्मा.cpp ModifyObjectPropertyresolves propertyID → table entry (short scan of ≤ 24 entries), then the flow is unchanged: re-validate,set(),dataVersion++,GeometryForObject, MODIFY push.
Storage changes
CommonNamedNumbers.h:kGeometry3DMvpSchemaVersion1 → 2,kGeometry3DLineMemberSchemaVersion2 → 3 (structstorageSchemaVersionconstants follow).DataStorage.cpp: the 14 affected Encode/Decode pairs become per-scalarmessage.set_center_x(...)/message.center_x()calls; colors keep the half↔float per-component conversion.WritePoint3/ReadPoint3/WriteColor4/ReadColor4/DefaultColor4remain only for the repeated-field paths (Pyramid/Cuboid/Parallelepiped/FrustumOfPyramid).- Load gate in
DeserializeGeometryObject: storedschema_versionolder than the type’s current constant → fail the load through the existingerrorMessagepath with a clear “saved by an older pre-release build; format no longer supported” message. GenerateDataStorageProtobuf.ps1and the vcxproj need no change (the proto file set is unchanged).
Renderer notes
- Snapshot array becomes
double[]. - Float-kind values must be formatted via
std::to_charsonstatic_cast<float>(value)— formatting a float-backed value as double would print noise digits (0.30000001192…) and break the seeded-string round-trip guarantee. - Parse path is unchanged: buffer → double → validate range → commit payload already carries
std::bit_cast<uint64_t>(double);set()casts to the stored type.
Explicitly unchanged (behavior-preservation checklist)
- Pane visuals, row set, and Float-only editing UX (same rows, same formatting, same validation outcomes).
- C++ struct memory layouts — zero struct changes;
META_DATAuntouched. - Selection flow, draft-validation reservation, input guards, copy-thread MODIFY protocol.
- SQLite schema, 2D record model, logical objects.
optionalFieldsFlags/ Optional64: untouched — per-proto-field property IDs are exactly the key the future presence system needs, so “many properties Optional, highly compact layout” plugs into this ID scheme without rework.
Implementation order
- Rewrite the 14 protos per the layouts above.
- Bump the two schema-version constants in
CommonNamedNumbers.h. - Rewrite the Encode/Decode pairs in
DataStorage.cpp; add the load gate. - Redesign
PropertyPane.h(kind enum, range constants, new descriptor, double-based signatures). - Add tables + validators next to the structs; delete the old tables from
PropertyPane.cpp. - Rebuild
PropertyPane.cpp: registry + lookup + validation + static_asserts. - Update
UserInterface.cpp(double snapshots, float-cast formatting, propertyID key),Main.cpprouting,UserInterface.hpayload comment,विश्वकर्मा.cpp ModifyObjectProperty. - Update this document (move this section into the main body as-built) and add a one-line
note in
storage.md§12 that 3D payload protos use flat scalars.
Verification (Windows build)
- Build regenerates protos automatically (pre-build protoc step).
- Create all 15 wired types → save → reopen → geometry identical.
- Edit Sphere radius + center end-to-end: geometry updates, pane re-reads the applied value.
- Reject 0/negative radius, NaN,
inside >= outsideon Pipe (red flash). - A
.yyysaved by a previous build fails to load with the clear version message. - Typing in a field still suppresses shortcuts (‘P’ spawns no pyramid).
Phased roadmap (after this iteration)
| Phase | Deliverable |
|---|---|
| 2 | Bool/Uint/Int/Double editable end-to-end (the u64 payload already fits); LineMember profile_id row; Elbow/Tee/Flange tables + UITextIDs; grow the 8-slot validator/snapshot buffers (Tee needs 12) |
| 3 | Utf8String kind: name proto fields + persistence (append new field numbers, schema bump), string commit channel UI→engineering, string accessor pair on the descriptor |
| 4 | Logical objects (Folder name/short_code) — pane reacts to data-tree selection, not just 3D picking |
| 5 | Optional64 presence integration: presence bit per proto-field ID, pane renders unset state |
Risks / notes
- Old
.yyyfiles become unreadable — accepted (pre-release); the load gate turns silent mis-decode into an explicit error. - Devanagari headers must remain UTF-8 (repo rule) — edits touch
डेटा-सामान्य-3D.h/डेटा-संरचना.h. doubleaccessors cap exact integers at 2^53 — irrelevant while only floats are editable; documented for phase 2’sprofile_id.- constexpr-lambda tables are standard C++20; the fallback above covers a compiler objection.