-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFrameTreeControl.cs
More file actions
1363 lines (1253 loc) · 72.4 KB
/
Copy pathFrameTreeControl.cs
File metadata and controls
1363 lines (1253 loc) · 72.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
using Autodesk.AutoCAD.Geometry;
using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;
namespace TimberDraw
{
// Tree-based structural editor for the Frame model. The FrameSpec is the source of truth.
// The tree starts EMPTY; right-click builds it: Add Bent / Add Bay (alternating; a bay may
// lead or trail the frame). Set a bent's Bent Type / a bay's Roof Type in the PropertyGrid;
// that fills the element's folder with ALL its timbers as checkboxes (all checked). Unchecking
// a timber drops it. The PropertyGrid shows only the SELECTED item's properties (a timber leaf
// shows that timber's sizes; a bent/bay node shows its top-level props). Edits persist the spec;
// the Draw button renders. Save/Save As/Load handle named .framespec templates.
public partial class FrameTreeControl
{
private FrameSpec _spec;
private string _currentPath; // last Save/Load path (for plain Save)
private bool _building; // suppress select/check handlers during (re)build
private ContextMenuStrip _menu;
// Multi-select of timber leaves (Ctrl/Shift-click). The membership checkboxes are a separate
// mechanism; these are the leaves whose COMMON properties are co-edited in the grid.
private readonly List<TreeNode> _selLeaves = new List<TreeNode>();
private TreeNode _anchorLeaf; // anchor for Shift-range selection
// Tree leaf tag for a timber: which owner (bent/bay) and the universal Timber itself.
private class TimberLeaf { public IMemberOwner Owner; public Timber Timber; }
// Tree leaf tag for a DRAWING-DERIVED timber under a Free Assembly element: a read-only view of
// a managed timber assigned via TAssign (get-only props -> the PropertyGrid shows them read-only).
private class DrawnLeaf
{
private readonly string _type, _desig, _size;
public DrawnLeaf(Module1.DataStructure xd)
{ _type = xd.Type ?? ""; _desig = xd.Designation ?? ""; _size = xd.Size ?? ""; }
[Category("Assigned"), DisplayName("Type")] public string Type => _type;
[Category("Assigned"), DisplayName("Designation")] public string Designation => _desig;
[Category("Assigned"), DisplayName("Size")] public string Size => _size;
public static string LabelFor(Module1.DataStructure xd)
{
string t = string.IsNullOrEmpty(xd.Type) ? "Timber" : xd.Type;
string d = string.IsNullOrEmpty(xd.Designation) ? "" : " " + xd.Designation;
string s = string.IsNullOrEmpty(xd.Size) ? "" : " (" + xd.Size + ")";
return t + d + s;
}
}
// Stable tags for the two container folders (Bents / Walls) so expand-state survives rebuilds
// and selecting them clears the grid rather than binding the raw list.
private readonly object _bentsTag = new object();
private readonly object _wallsTag = new object();
// Stable tag for the per-element "Assigned" folders (batch-3 #5) -- same expand-state deal.
private readonly object _assignedTag = new object();
public FrameTreeControl()
{
InitializeComponent();
Load += FrameTreeControl_Load;
}
private static TimberDraw.Properties.Settings S => TimberDraw.Properties.Settings.Default;
private void FrameTreeControl_Load(object sender, EventArgs e)
{
SetupMenu();
WireEvents();
ResetToEmpty(); // the deterministic start; then...
TryRecallDrawing(); // ...refill from the ACTIVE drawing's stamped recipe when it has one
// Re-render distance cells when the drawing's units change (DistanceConverter formats via
// LUNITS/LUPREC). Reset to the empty start when the active drawing changes (the start is
// deterministic + drawing-clean). Drop both handlers with the control to avoid dangling subs.
AcadApp.SystemVariableChanged += OnSysVarChanged;
AcadApp.DocumentManager.DocumentActivated += OnDocumentActivated;
Disposed += (s, ev) =>
{
AcadApp.SystemVariableChanged -= OnSysVarChanged;
AcadApp.DocumentManager.DocumentActivated -= OnDocumentActivated;
};
}
private void OnSysVarChanged(object sender, Autodesk.AutoCAD.ApplicationServices.SystemVariableChangedEventArgs e)
{
if (e.Name == "LUNITS" || e.Name == "LUPREC") BindGrid(); // re-format distances per units
}
// Switching drawings restarts the tree from the empty start (drawing-clean contract), then
// recalls the NEW drawing's own frame when it carries one (batch-3 #3).
private void OnDocumentActivated(object sender, Autodesk.AutoCAD.ApplicationServices.DocumentCollectionEventArgs e)
{
if (e.Document == null) return; // last document closed -> nothing to reflect
try { ResetToEmpty(); TryRecallDrawing(); } catch { /* best-effort; activation can fire mid-init */ }
}
// Recall-on-open (Robert's ask, batch-3 #3): a drawing whose frame was drawn by the tree
// carries its recipe in the frame registry record (stamped at every Draw), so activating
// that drawing refills the tree with ITS frame -- exact recall, nothing derived from the
// solids (the spec is a generator, not the model). Drawings from before the stamp, or
// with no drawn frame, keep the empty start.
private void TryRecallDrawing()
{
try
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) return;
string json = FrameRegistry.FirstSpecJson(doc.Database);
if (string.IsNullOrEmpty(json)) return;
_spec = FrameSpecStore.FromJson(json);
_currentPath = null;
Persist(); // sync the FrameSpecJson signal (TGrid's frame tag) to the recalled frame
BuildTree();
RefreshFrozenState();
}
catch (System.Exception ex)
{ Diag.Warn("FrameTree.Recall", "drawing spec recall failed (tree stays empty): " + ex.Message); }
}
// The start state everywhere (open, doc-switch, AND the New button): an EMPTY tree.
// No seed exists anymore -- grow via right-click Add Bent / Add Wall or Load a
// .framespec. Deliberately does NOT Persist: FrameSpecJson (the active-frame-tag signal
// sibling commands like TGrid read) keeps the last real frame until a new spec exists.
public void ResetToEmpty()
{
_spec = new FrameSpec();
_currentPath = null;
propPane.Clear();
BuildTree();
RefreshFrozenState(); // empty gate: Draw/Freeze disabled until the tree has content
}
private void WireEvents()
{
treeView.AfterSelect += (s, e) => { if (!_building) SyncFromSingle(e.Node); };
treeView.AfterCheck += TreeView_AfterCheck;
treeView.NodeMouseClick += TreeView_NodeMouseClick;
propPane.ValueCommitted += HandlePropChange;
// Brace rows render their labels as the solve-for checkboxes -- ONLY on brace leaves
// (a strut's Angle row keeps its plain label).
propPane.RowChecked = name =>
{
if (name != "Foot" && name != "Head" && name != "Angle") return null;
if (!(treeView.SelectedNode?.Tag is TimberLeaf tl) || !IsBraceRole(tl.Timber.Role)) return null;
return _braceMask.Contains(name);
};
propPane.RowCheckToggled += OnBraceMaskToggle;
ButtonDraw.Click += (s, e) => DrawFrame();
ButtonNew.Click += (s, e) => NewFrame();
ButtonSave.Click += (s, e) => SaveSpec(false);
ButtonSaveAs.Click += (s, e) => SaveSpec(true);
ButtonLoad.Click += (s, e) => LoadFromFile();
ButtonSetDefault.Click += (s, e) => SetAsDefault();
ButtonFreeze.Click += (s, e) => FreezeFrame();
// TGrid reads the drawing + FrameSpecJson in a command context -- fire it like the
// other palette verbs rather than calling into the DB from a WinForms handler.
ButtonGrid.Click += (s, e) =>
AcadApp.DocumentManager.MdiActiveDocument?.SendStringToExecute("TGrid ", true, false, true);
}
// Reflect the active frame's freeze gate (the break): a frozen frame's parametric generator is
// locked, so Draw won't re-emit and there is nothing left to freeze. Called on load and whenever
// the palette is (re)shown. The authoritative lock is DrawFrame's runtime guard; this is the UI.
public void RefreshFrozenState()
{
// Empty tree (the open / doc-switch start state): nothing to draw or freeze yet.
if (SpecIsEmpty)
{
ButtonDraw.Enabled = false;
ButtonFreeze.Enabled = false;
ButtonFreeze.Text = "Freeze";
return;
}
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null)
{
// Zero-document state: nothing can draw or freeze. A defined state matters here --
// the empty branch above may have disabled the buttons, and without this a New/Load
// in a doc-less session would leave them stuck. Document activation re-runs this.
ButtonDraw.Enabled = false;
ButtonFreeze.Enabled = false;
ButtonFreeze.Text = "Freeze";
return;
}
bool frozen;
try { frozen = FrameRegistry.IsFrozen(doc.Database, FrameTagSafe()); }
catch { return; } // leave the buttons as-is if the frame can't be read
ButtonDraw.Enabled = !frozen; // a frozen frame won't re-emit (generator locked)
ButtonFreeze.Enabled = !frozen; // one-way; nothing to do once frozen
ButtonFreeze.Text = frozen ? "Frozen" : "Freeze";
}
// Empty = no structural content yet (the start state). A spec becomes non-empty the
// moment a bent or wall exists, whether via New's starter seed, Load, or right-click.
private bool SpecIsEmpty =>
_spec == null || (_spec.Bents.Count == 0 && _spec.Walls.Count == 0);
// Freeze (the BREAK): lock this frame's parametric generator. Afterward Draw refuses to re-emit
// and the skeleton timbers carry on as the managed-editor truth -- geometry is untouched, only
// the gate flips. One-way. Mirrors the TFreeze command, but freezes THIS frame (the spec's tag)
// directly, with no command-line tag prompt.
private void FreezeFrame()
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) return;
var db = doc.Database;
string frame = FrameTagSafe();
if (FrameRegistry.IsFrozen(db, frame))
{ Dialogs.Info("Frame " + frame + " is already frozen."); RefreshFrozenState(); return; }
if (ManagedTimber.EnumerateFrameFrames(db, frame).Count == 0)
{ Dialogs.Info("Nothing to freeze in frame " + frame + " -- draw it first."); return; }
if (!Dialogs.ConfirmWarn(
"Freeze frame " + frame + "? This is the one-way break: the parametric palette locks, "
+ "Draw stops re-emitting, and the timbers carry on as the managed-editor truth "
+ "(edit them with the managed verbs)."))
return;
FrameRegistry.SetFrozen(db, frame, true);
// The tree still holds this frame's recipe at the break -- stamp it (recall-on-open)
// in case the drawing was drawn before the Draw-time stamp existed.
try
{
FrameRecord rec = FrameRegistry.Load(db, frame) ?? new FrameRecord { Frozen = true };
if (string.IsNullOrEmpty(rec.SpecJson))
{ rec.SpecJson = FrameSpecStore.ToJson(_spec); FrameRegistry.Save(db, frame, rec); }
}
catch (System.Exception ex)
{ Diag.Warn("FrameTree.Freeze", "spec stamp failed (recall-on-open unavailable): " + ex.Message); }
doc.Editor.WriteMessage("\nTFreeze: frame " + frame +
" frozen -- parametric palette locked; edit via the managed verbs.");
RefreshFrozenState();
}
// The PropertyGrid shows ONLY the selected item's properties:
// - Frame root -> the FrameSpec (its Browsable props are just frame geometry).
// - Bent node -> Name, Bent Type, + type-global params (Divisor / Queen position).
// - Bay node -> Name, Spacing, Roof Type.
// - Timber leaf -> just that timber's size fields (edits write through to the owner).
private object SelectedTarget(TreeNode n)
{
object tag = n?.Tag;
if (tag is FrameSpec fs)
return new FilteredView(fs, new[] { "FrameTag", "Span", "EaveHt", "PitchRise" });
if (ReferenceEquals(tag, _bentsTag)) return BentRoster();
if (ReferenceEquals(tag, _wallsTag)) return WallRoster();
if (tag is BentSpec b) return new FilteredView(b, BentNodeProps(b));
if (tag is WallSpec w) return new FilteredView(w, new[] { "Name", "Separation" });
if (tag is BaySpec y) return new FilteredView(y, new[] { "Name", "BraceCentering" });
if (tag is TimberLeaf tl) return new TimberMemberView(LeafRows(tl));
return tag;
}
// ------------------------------------------------------- multi-select
// Left-click with Ctrl/Shift extends the leaf selection; a plain click / keyboard nav goes
// through AfterSelect -> SyncFromSingle (single-select). Right-click selects for the menu.
private void TreeView_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
{
if (e.Button == MouseButtons.Right) { treeView.SelectedNode = e.Node; return; }
if (e.Button != MouseButtons.Left) return;
Keys mod = Control.ModifierKeys;
bool ctrl = (mod & Keys.Control) != 0;
bool shift = (mod & Keys.Shift) != 0;
if ((!ctrl && !shift) || !(e.Node.Tag is TimberLeaf)) return; // AfterSelect handled it
if (ctrl)
{
if (_selLeaves.Contains(e.Node)) _selLeaves.Remove(e.Node);
else _selLeaves.Add(e.Node);
_anchorLeaf = e.Node;
}
else // shift: contiguous run of leaves between the anchor and this node, in tree order
{
var leaves = new List<TreeNode>();
foreach (TreeNode n in AllNodes(treeView.Nodes))
if (n.Tag is TimberLeaf) leaves.Add(n);
int a = _anchorLeaf != null ? leaves.IndexOf(_anchorLeaf) : -1;
int b = leaves.IndexOf(e.Node);
if (a < 0) a = b;
if (a > b) { int t = a; a = b; b = t; }
_selLeaves.Clear();
for (int i = a; i <= b; i++) _selLeaves.Add(leaves[i]);
}
RepaintSelection();
BindGrid();
}
// Plain click / keyboard nav: the selection is just this node (a leaf -> single co-edit set).
private void SyncFromSingle(TreeNode node)
{
_selLeaves.Clear();
if (node?.Tag is TimberLeaf) _selLeaves.Add(node);
_anchorLeaf = node;
RepaintSelection();
BindGrid();
}
// Bind the grid to the current selection: 1+ leaves -> common props across all of them;
// a non-leaf node -> its own top-level props. Everything (braces included) is the one grid.
private void BindGrid()
{
if (_building) return;
if (_selLeaves.Count > 0)
{
var views = new List<TimberMemberView>();
foreach (TreeNode n in _selLeaves)
views.Add(new TimberMemberView(LeafRows((TimberLeaf)n.Tag)));
propPane.Bind(views.ToArray());
}
else
{
propPane.Bind(SelectedTarget(treeView.SelectedNode));
}
}
private static bool IsBraceRole(string role)
=> role == "Brace" || role == "FloorBrace" || role == "EaveBrace"
|| role == "QueenBrace" || role == "CollarBrace"
|| role == "RidgeBrace" || role == "QueenGirtBrace" || role == "HPostGirtBrace";
// Brace solve-for mask (the Assembly-tab mechanic): exactly TWO of Foot/Head/Angle are the
// inputs; the third is derived + read-only. Session-sticky and shared across brace leaves
// (the VALUES stay per member); insertion order = the Assembly rule, checking a third drops
// the oldest. Angle convention follows the Assembly tab: tan(angle) = head / foot.
private static readonly List<string> _braceMask = new List<string> { "Foot", "Head" };
// Keep exactly two checked: checking a third drops the oldest; unchecking one of two is
// refused (you switch by CHECKING the third) -- BindGrid re-renders the refused box checked.
private void OnBraceMaskToggle(string name, bool on)
{
if (on && !_braceMask.Contains(name))
{
_braceMask.Add(name);
while (_braceMask.Count > 2) _braceMask.RemoveAt(0);
}
foreach (TreeNode ln in _selLeaves)
if (ln.Tag is TimberLeaf tl && IsBraceRole(tl.Timber.Role))
RecomputeBrace(tl.Timber.Size);
Persist();
BindGrid();
}
// Derive the masked-out value from the two active ones (the Assembly RecomputeBrace rules);
// Length always follows. Angle clamps to (0.1, 89.9) and rounds to 0.01.
private static void RecomputeBrace(MemberSize s)
{
bool f = _braceMask.Contains("Foot"), h = _braceMask.Contains("Head");
if (f && h)
s.Angle = System.Math.Round(Math.Atan2(s.Head, s.Foot) * 180.0 / Math.PI, 2);
else
{
s.Angle = ClampRoundAngle(s.Angle);
double t = Math.Tan(s.Angle * Math.PI / 180.0);
if (f) s.Head = s.Foot * t;
else s.Foot = s.Head / t;
}
s.Length = Math.Sqrt(s.Foot * s.Foot + s.Head * s.Head);
}
private static double ClampRoundAngle(double v)
=> System.Math.Round(Math.Max(0.1, Math.Min(89.9, v)), 2);
// --------------------------------------------------- group-layer isolation
private string FrameTagSafe() => string.IsNullOrWhiteSpace(_spec.FrameTag) ? "A" : _spec.FrameTag.Trim();
private string GroupLayerFor(BentSpec b)
=> ManagedTimber.GroupLayer(FrameTagSafe(), (_spec.Bents.IndexOf(b) + 1).ToString(), "");
private string GroupLayerFor(WallSpec w)
=> ManagedTimber.GroupLayer(FrameTagSafe(), "", w.Letter);
private bool IsGroupVisible(string layer)
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
return doc == null || ManagedTimber.IsGroupVisible(doc.Database, layer);
}
private void SetGroupVisible(string layer, bool visible)
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc != null) ManagedTimber.SetGroupVisible(doc.Database, layer, visible);
}
private void ToggleGroupLayer(string layer, bool visible) { SetGroupVisible(layer, visible); RegenView(); }
private static void RegenView()
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
try { doc?.Editor.Regen(); } catch { /* layer On/Off auto-updates; regen is best-effort */ }
}
private static bool AllChecked(TreeNode parent)
{
foreach (TreeNode c in parent.Nodes) if (!c.Checked) return false;
return parent.Nodes.Count > 0;
}
// Paint the multi-select set (blue highlight; the user is red/green colour-blind, never green).
private void RepaintSelection()
{
foreach (TreeNode n in AllNodes(treeView.Nodes))
if (n.Tag is TimberLeaf)
{
bool sel = _selLeaves.Contains(n);
n.BackColor = sel ? Theme.Accent : System.Drawing.Color.Empty;
n.ForeColor = sel ? System.Drawing.Color.Black : System.Drawing.Color.Empty;
}
}
// ----------------------------------------------------------------- tree
private void BuildTree()
{
_building = true;
try
{
// Preserve the user's expand/collapse state across rebuilds (element + root Tags are the
// same object refs after the rebuild; timber leaves have no children). Don't ExpandAll.
var expanded = new System.Collections.Generic.HashSet<object>();
bool firstBuild = treeView.Nodes.Count == 0;
foreach (TreeNode n in AllNodes(treeView.Nodes))
if (n.IsExpanded && n.Tag != null) expanded.Add(n.Tag);
_selLeaves.Clear(); _anchorLeaf = null; // nodes are recreated below
treeView.Nodes.Clear();
TreeNode root = new TreeNode(FrameLabel(_spec)) { Tag = _spec };
treeView.Nodes.Add(root);
// Axis 1: Bents -> Bent N -> cross-section member leaves.
TreeNode bentsFolder = new TreeNode("Bents") { Tag = _bentsTag };
root.Nodes.Add(bentsFolder);
foreach (BentSpec b in _spec.Bents)
{
// Node checkbox = its group LAYER visibility (isolation), read from the drawing.
TreeNode node = new TreeNode(NodeLabel(b)) { Tag = b, Checked = IsGroupVisible(GroupLayerFor(b)) };
bentsFolder.Nodes.Add(node);
AddTimberLeaves(node, b);
}
bentsFolder.Checked = AllChecked(bentsFolder);
// Axis 2: Walls -> Wall A/B -> Bay I/II -> longitudinal member leaves.
TreeNode wallsFolder = new TreeNode("Walls") { Tag = _wallsTag };
root.Nodes.Add(wallsFolder);
foreach (WallSpec w in _spec.Walls)
{
TreeNode wnode = new TreeNode(NodeLabel(w)) { Tag = w, Checked = IsGroupVisible(GroupLayerFor(w)) };
wallsFolder.Nodes.Add(wnode);
// A Free Assembly wall has no parametric bays -- list its assigned timbers directly.
if (w.FreeAssembly) { AddDrawnLeaves(wnode, "", w.Letter, false); continue; }
// An addressing-only line (a KingPost vstrut B/D) carries no longitudinal members --
// show the line, but no (empty) bay nodes. Assigned FREE timbers still list.
if (w.Role == BayRole.Vstrut) { AddAssignedFolder(wnode, "", w.Letter); continue; }
foreach (BaySpec y in w.Bays)
{
TreeNode ynode = new TreeNode(NodeLabel(y)) { Tag = y };
wnode.Nodes.Add(ynode);
AddTimberLeaves(ynode, y);
}
AddAssignedFolder(wnode, "", w.Letter); // batch-3 #5: the wall's assigned free timbers
}
wallsFolder.Checked = AllChecked(wallsFolder);
if (firstBuild) { root.Expand(); bentsFolder.Expand(); wallsFolder.Expand(); }
else
foreach (TreeNode n in AllNodes(treeView.Nodes))
if (n.Tag != null && expanded.Contains(n.Tag)) n.Expand();
}
finally { _building = false; }
// Re-apply the empty gate: the first Add Bent (right-click) or a Load flips the tree
// non-empty, and Draw/Freeze must follow.
RefreshFrozenState();
}
private void TreeView_AfterCheck(object sender, TreeViewEventArgs e)
{
if (_building) return;
object ntag = e.Node.Tag;
// Bent/Wall node + Bents/Walls container checkboxes toggle that group's LAYER visibility
// (isolation) -- not a spec change. Timber-leaf checkboxes (below) are member enablement.
if (ntag is BentSpec nb) { ToggleGroupLayer(GroupLayerFor(nb), e.Node.Checked); return; }
if (ntag is WallSpec nw) { ToggleGroupLayer(GroupLayerFor(nw), e.Node.Checked); return; }
if (ReferenceEquals(ntag, _bentsTag) || ReferenceEquals(ntag, _wallsTag))
{
bool vis = e.Node.Checked;
_building = true;
foreach (TreeNode child in e.Node.Nodes)
{
child.Checked = vis;
string ln = (child.Tag is BentSpec cb) ? GroupLayerFor(cb)
: (child.Tag is WallSpec cw) ? GroupLayerFor(cw) : null;
if (ln != null) SetGroupVisible(ln, vis);
}
_building = false;
RegenView();
return;
}
if (!(e.Node.Tag is TimberLeaf leaf))
{
// Frame root / other: no meaningful check (active-frame deferred); revert silently.
_building = true; e.Node.Checked = false; _building = false;
return;
}
leaf.Timber.Enabled = e.Node.Checked;
// A bay's Commons/Purlins are mutually exclusive: checking one unchecks the other.
string key = leaf.Timber.Key;
if (e.Node.Checked && leaf.Owner is BaySpec && (key == "Commons:X" || key == "Purlins:X"))
{
string otherKey = key == "Commons:X" ? "Purlins:X" : "Commons:X";
foreach (TreeNode sib in e.Node.Parent.Nodes)
if (sib.Tag is TimberLeaf sl && sl.Timber.Key == otherKey)
{
if (sib.Checked) { _building = true; sib.Checked = false; _building = false; }
sl.Timber.Enabled = false;
}
}
// A bay derives its RoofType from the roof checkboxes; resync + relabel, and if the roof
// actually changed overlay that roof's per-type default (like setting a bent's type).
if (leaf.Owner is BaySpec bay)
{
string before = bay.RoofType;
bay.SyncRoofType();
if (e.Node.Parent != null) e.Node.Parent.Text = NodeLabel(bay);
if (bay.RoofType != before)
{
// Roof checkbox changed: overlay ONLY the roof members/params -- the full
// overlay was unchecking the bay's floor girt + braces.
TypeDefaults.ApplyRoofOnly(bay);
Persist();
BuildTree();
SelectByTag(bay);
return;
}
}
Persist();
}
// Once typed, an owner's folder lists ALL its timbers as checkboxes; checked = enabled. A Free
// Assembly bent has no parametric timbers -- it lists the managed timbers assigned to its number.
// A TYPED bent additionally lists its assigned FREE timbers under an "Assigned" folder
// (batch-3 #5) -- free-only, so the recipe's own skeleton members don't double up.
private void AddTimberLeaves(TreeNode node, IMemberOwner owner)
{
if (!owner.TypeIsSet) return;
if (owner is BentSpec bs && bs.IsFreeAssembly)
{
AddDrawnLeaves(node, (_spec.Bents.IndexOf(bs) + 1).ToString(), "", false);
return;
}
foreach (Timber t in owner.Timbers)
node.Nodes.Add(new TreeNode(t.Label)
{
Tag = new TimberLeaf { Owner = owner, Timber = t },
Checked = t.Enabled
});
if (owner is BentSpec tb)
AddAssignedFolder(node, (_spec.Bents.IndexOf(tb) + 1).ToString(), "");
}
// The "Assigned" folder under a TYPED bent/wall (batch-3 #5): the same read-only DrawnLeaf
// view the Free Assembly elements always had, held apart from the recipe's checkbox leaves.
// FREE timbers only (hand-placed / adopted / floor infill) -- the generator's own skeleton
// members carry the same address and would otherwise list twice. Added only when the
// drawing actually has matches (no empty folders).
private void AddAssignedFolder(TreeNode node, string bentTag, string wallTag)
{
var folder = new TreeNode("Assigned") { Tag = _assignedTag };
AddDrawnLeaves(folder, bentTag, wallTag, true);
if (folder.Nodes.Count > 0) node.Nodes.Add(folder);
}
// List (read-only) the managed timbers in the drawing assigned to this frame + bent number
// (bentTag) or wall letter (wallTag) via TAssign. The tree is a VIEW of the drawing here --
// geometry editing stays in the managed commands (TPlace/TSpan/...). freeOnly limits the
// list to hand-placed survivors (Free == "1" or a FloorTag -- EraseFrame's keep test), for
// the typed-owner Assigned folders.
private void AddDrawnLeaves(TreeNode node, string bentTag, string wallTag, bool freeOnly)
{
var doc = AcadApp.DocumentManager.MdiActiveDocument;
if (doc == null) return;
string frame = string.IsNullOrWhiteSpace(_spec.FrameTag) ? "A" : _spec.FrameTag.Trim();
foreach (var (id, _) in ManagedTimber.Enumerate(doc.Database))
{
Module1.DataStructure xd = Module1.GetXdata(id);
if (xd == null) continue;
string ft = string.IsNullOrWhiteSpace(xd.FrameTag) ? "" : xd.FrameTag.Trim();
if (ft != frame) continue;
if (freeOnly && xd.Free != "1" && xd.Free != "2" && string.IsNullOrEmpty(xd.FloorTag)) continue;
bool match = bentTag.Length > 0
? (xd.BentNumber ?? "") == bentTag
: string.Equals(xd.WallTag ?? "", wallTag, StringComparison.OrdinalIgnoreCase);
if (!match) continue;
node.Nodes.Add(new TreeNode(DrawnLeaf.LabelFor(xd)) { Tag = new DrawnLeaf(xd) });
}
}
// The Frame root label carries its Name, e.g. "Frame (North Barn)"; unnamed -> "Frame (set name)".
private static string FrameLabel(FrameSpec s)
=> "Frame (" + (string.IsNullOrWhiteSpace(s.FrameTag) ? "set name" : s.FrameTag.Trim()) + ")";
// Read-only rosters shown when the Bents / Walls container is selected: each member + its
// offset from origin (the running sum of Separation). Offsets honor the drawing's units.
private object BentRoster()
{
var rows = new List<(string, double)>();
double z = 0;
foreach (BentSpec b in _spec.Bents) { z += b.Separation; rows.Add((b.Name, z)); } // Separation = gap from the previous bent
return new RosterView("Bents", rows);
}
private object WallRoster()
{
var rows = new List<(string, double)>();
double x = 0;
foreach (WallSpec w in _spec.Walls) { rows.Add((w.Name, x)); x += w.Separation; }
return new RosterView("Walls", rows);
}
private static string NodeLabel(FrameElement el)
{
if (el is BentSpec b) return "Bent " + b.Name + (b.TypeIsSet ? " (" + b.BentType + ")" : " (None)");
if (el is WallSpec w) return "Wall " + w.Name;
// Roof tag on EAVE bays (each eave owns its slope's commons/purlins); the center / vstrut
// lines carry no commons, so they read plain.
if (el is BaySpec y) return y.Role == BayRole.Eave ? y.Name + " (" + y.RoofShort() + ")" : y.Name;
return el.Name;
}
// A property pane edit committed (arg = the descriptor's Name). Type change -> rebuild the
// folder. Name change -> relabel the node. A leaf Foot/Head -> recompute reported Length/Angle.
// Otherwise just persist.
private void HandlePropChange(string name)
{
TreeNode node = treeView.SelectedNode;
object tag = node?.Tag;
if ((name == "BentType" || name == "RoofType") && tag is IMemberOwner owner)
{
owner.RebuildTimbers(); // factory emits the type's default timbers (all enabled)
TypeDefaults.Apply(owner); // overlay the saved per-type template if one exists
if (name == "BentType") _spec.SyncWallRoles(); // re-role B/D (queen / hammer / vstrut)
Persist();
BuildTree();
SelectByTag(tag);
return;
}
// A HammerBeam divisor change can add/remove interior wall lines (div6 stacks two hammer-post
// tiers per side -> A-G), so reconcile the wall set and redraw the tree.
if (name == "HBDivisor" && tag is BentSpec)
{
_spec.SyncWallRoles();
Persist();
BuildTree();
SelectByTag(tag);
return;
}
// Leaf edits: the descriptor Name is the canonical label (Width/Foot/Head/Label/...).
if (_selLeaves.Count > 0)
{
if (name == "Foot" || name == "Head" || name == "Angle")
{
// Brace solve-for: the two mask-active inputs drive the derived third (the
// Assembly-tab mechanic). Struts' Angle edits pass through untouched.
foreach (TreeNode ln in _selLeaves)
if (ln.Tag is TimberLeaf tl && IsBraceRole(tl.Timber.Role))
RecomputeBrace(tl.Timber.Size);
BindGrid(); // re-render the derived rows
}
else if (name == "Name")
{
foreach (TreeNode ln in _selLeaves)
if (ln.Tag is TimberLeaf tl) ln.Text = tl.Timber.Label;
}
Persist();
return;
}
if (name == "Name" && node != null && tag is FrameElement fe)
node.Text = NodeLabel(fe);
if (name == "FrameTag" && node != null && tag is FrameSpec fs2)
node.Text = FrameLabel(fs2);
Persist();
}
// --------------------------------------------------------- context menu
private void SetupMenu()
{
_menu = new ContextMenuStrip(this.components);
_menu.Opening += (s, e) => BuildMenu(e);
treeView.ContextMenuStrip = _menu;
}
private void BuildMenu(System.ComponentModel.CancelEventArgs e)
{
_menu.Items.Clear();
object tag = treeView.SelectedNode?.Tag;
// First-element affordances (the empty start state): the root and the Bents/Walls
// containers offer the initial Add so the tree can grow incrementally without the
// New starter seed. Once an element exists, Insert Before/After takes over.
if (tag is FrameSpec || ReferenceEquals(tag, _bentsTag) || ReferenceEquals(tag, _wallsTag))
{
if (_spec.Bents.Count == 0 && !ReferenceEquals(tag, _wallsTag))
_menu.Items.Add(new ToolStripMenuItem("Add Bent", null, (s, a) => AddFirstBent()));
if (_spec.Walls.Count == 0 && !ReferenceEquals(tag, _bentsTag))
_menu.Items.Add(new ToolStripMenuItem("Add Wall", null, (s, a) => AddFirstWall()));
}
// Menus otherwise only on Bent/Wall element nodes -- Insert Before/After (split the gap or
// extend at an end) + Remove (kept >= 1 so the tree always has a node to grow from).
// Timber leaves get no menu.
if (tag is BentSpec bent)
{
_menu.Items.Add(new ToolStripMenuItem("Insert Bent Before", null, (s, a) => InsertBentCmd(bent, true)));
_menu.Items.Add(new ToolStripMenuItem("Insert Bent After", null, (s, a) => InsertBentCmd(bent, false)));
AddInsertFromTemplate(bent);
if (_spec.Bents.Count > 1)
_menu.Items.Add(new ToolStripMenuItem("Remove Bent", null, (s, a) => RemoveBent(bent)));
_menu.Items.Add(new ToolStripSeparator());
AddTemplateItems(bent);
}
else if (tag is WallSpec wall)
{
_menu.Items.Add(new ToolStripMenuItem("Insert Wall Before", null, (s, a) => InsertWallCmd(wall, true)));
_menu.Items.Add(new ToolStripMenuItem("Insert Wall After", null, (s, a) => InsertWallCmd(wall, false)));
if (_spec.Walls.Count > 1)
_menu.Items.Add(new ToolStripMenuItem("Remove Wall", null, (s, a) => RemoveWallCmd(wall)));
}
else if (tag is BaySpec bay)
{
AddTemplateItems(bay); // bays come from walls -> apply a template onto an existing bay cell
}
if (_menu.Items.Count == 0) e.Cancel = true; // nothing to show -> suppress the menu
}
// ---- named template library (Save / Apply / Insert-from / Manage) -------------------------
// The named TemplateLibrary, distinct from the per-type "Set as Default" button (which writes
// TypeDefaults). Templates carry the bent/bay CONFIG (sizes + which members are on + the type params);
// the frame supplies span / eave / pitch / spacing, so a template re-fits any frame.
private void AddTemplateItems(FrameElement el)
{
_menu.Items.Add(new ToolStripMenuItem("Save as Template...", null, (s, a) => SaveTemplate(el)));
var apply = new ToolStripMenuItem("Apply Template");
foreach (string name in TemplateLibrary.CompatibleNames(el))
{
string n = name;
apply.DropDownItems.Add(new ToolStripMenuItem(n, null, (s, a) => ApplyTemplate((IMemberOwner)el, n)));
}
apply.Enabled = apply.DropDownItems.Count > 0;
_menu.Items.Add(apply);
_menu.Items.Add(new ToolStripMenuItem("Manage Templates...", null, (s, a) => Dialogs.ManageTemplates(this)));
}
// "Insert Bent from Template >": every Bent template; inserts a NEW bent after `sel`, types it, overlays.
private void AddInsertFromTemplate(BentSpec sel)
{
var ins = new ToolStripMenuItem("Insert Bent from Template");
foreach ((string Name, string Type) t in TemplateLibrary.All())
if (t.Type != null && t.Type.StartsWith("Bent:"))
{
string n = t.Name;
ins.DropDownItems.Add(new ToolStripMenuItem(n, null, (s, a) => InsertBentFromTemplate(sel, n)));
}
ins.Enabled = ins.DropDownItems.Count > 0;
_menu.Items.Add(ins);
}
private void SaveTemplate(FrameElement el)
{
if (TypeDefaults.Key(el) == null)
{ Dialogs.Info("Set the bent/bay type first, then Save as Template."); return; }
string defName = el is BentSpec b ? "My " + b.BentType + " Bent"
: el is BaySpec y ? "My " + y.RoofShort() + " Bay" : "My Template";
if (!Dialogs.PromptText(this, "Template name:", defName, out string name)) return;
if (TemplateLibrary.Exists(name) &&
!Dialogs.Confirm("Replace the existing template \"" + name + "\"?"))
return;
if (TemplateLibrary.Save(name, el))
AcadApp.DocumentManager.MdiActiveDocument?.Editor.WriteMessage("\nSaved template \"" + name + "\".");
}
private void ApplyTemplate(IMemberOwner owner, string name)
{
if (!TemplateLibrary.TryGet(name, out FrameElement saved)) return;
// An UNTYPED bent adopts the template's type first -- the canonical member list must
// exist before the overlay (same order as Insert Bent from Template). A no-roof bay
// needs nothing here: its timber list is fixed, and ApplyElement copies the roof
// checkboxes + SyncRoofType()s, which IS setting the roof.
if (owner is BentSpec b && !b.TypeIsSet && saved is BentSpec sb)
{
b.BentType = sb.BentType;
b.RebuildTimbers();
_spec.SyncWallRoles();
}
TypeDefaults.ApplyElement(owner, saved); // overlays sizes + config; never span/eave/pitch/Separation
Persist(); BuildTree(); SelectByTag(owner);
}
private void InsertBentFromTemplate(BentSpec sel, string name)
{
if (!TemplateLibrary.TryGet(name, out FrameElement saved) || !(saved is BentSpec sb)) return;
BentSpec nb = _spec.InsertBent(sel, false, DefaultBentInsertDistance(sel, false));
if (nb == null) { Dialogs.Info("Could not insert the bent (invalid distance)."); return; }
nb.BentType = sb.BentType;
nb.RebuildTimbers(); // canonical members for the type
TypeDefaults.ApplyElement(nb, saved); // overlay the template's sizes + config
Persist(); BuildTree(); SelectByTag(nb);
}
// --------------------------------------------------------- mutations
private void RemoveBent(BentSpec b)
{
if (!_spec.RemoveBent(b)) return;
Persist();
BuildTree();
}
// The first bent / first wall of an empty tree (the root/container menu items). No
// distance prompt -- there is nothing to measure from yet; separations come with the
// second element via Insert Before/After.
private void AddFirstBent()
{
BentSpec nb = _spec.AddBent();
Persist(); BuildTree(); SelectByTag(nb);
}
private void AddFirstWall()
{
WallSpec nw = _spec.AddWall();
Persist(); BuildTree(); SelectByTag(nw);
}
// NO distance dialog (Robert's call -- Separation is already a bent property in the pane):
// the insert takes a DEFAULT and the pane fine-tunes it. An interior insert splits the bay
// in half; extending the frame repeats its bay rhythm (the last non-zero separation).
private double DefaultBentInsertDistance(BentSpec sel, bool before)
{
double gap = _spec.GapForBentInsert(sel, before, out bool interior);
if (interior) return gap / 2.0;
for (int i = _spec.Bents.Count - 1; i >= 0; i--)
if (_spec.Bents[i].Separation > 1e-9) return _spec.Bents[i].Separation;
return 96.0; // a lone-bent frame has no rhythm yet: the stock 8' bay
}
private void InsertBentCmd(BentSpec sel, bool before)
{
BentSpec nb = _spec.InsertBent(sel, before, DefaultBentInsertDistance(sel, before));
if (nb == null) { Dialogs.Info("Could not insert the bent (invalid distance)."); return; }
Persist(); BuildTree(); SelectByTag(nb);
}
private void InsertWallCmd(WallSpec sel, bool before)
{
double gap = _spec.GapForWallInsert(sel, before, out bool interior);
string prompt = interior
? "Distance from the upstream wall (< " + Fmt(gap) + "), splits the span:"
: "Separation for the new wall (extends the span):";
if (!Dialogs.PromptDistance(this, prompt, interior ? gap : 0.0, out double d)) return;
WallSpec nw = _spec.InsertWall(sel, before, d);
if (nw == null) { Dialogs.Info("Could not insert the wall (invalid distance)."); return; }
Persist(); BuildTree(); SelectByTag(nw);
}
private void RemoveWallCmd(WallSpec w)
{
if (!_spec.RemoveWall(w)) return;
Persist();
BuildTree();
}
private static string Fmt(double inches) => inches.ToString("0.##") + "\"";
private void SelectByTag(object tag)
{
foreach (TreeNode n in AllNodes(treeView.Nodes))
if (ReferenceEquals(n.Tag, tag)) { treeView.SelectedNode = n; return; }
}
private static IEnumerable<TreeNode> AllNodes(TreeNodeCollection nodes)
{
foreach (TreeNode n in nodes)
{
yield return n;
foreach (TreeNode c in AllNodes(n.Nodes)) yield return c;
}
}
// ------------------------------------------------------------- actions
private void DrawFrame()
{
// One-way gate: a frozen frame's generator is locked -- never re-emit over hand edits. The
// button is disabled when frozen (RefreshFrozenState); this is the matching runtime guard in
// case the gate flipped elsewhere (the TFreeze command / the TPanel button) while shown.
var gate = AcadApp.DocumentManager.MdiActiveDocument;
if (gate != null && FrameRegistry.IsFrozen(gate.Database, FrameTagSafe()))
{
Dialogs.Info("Frame " + FrameTagSafe() + " is frozen -- the parametric generator is locked. "
+ "Edit the timbers with the managed verbs, or start a new frame.");
RefreshFrozenState();
return;
}
// ONE FRAME PER DRAWING (Robert's call, 2026-07-16): Draw erases only its OWN tag, so a
// renamed spec Name would silently ADD a second frame beside the existing one -- warn
// and let him back out (a frame-rename op is a future ask if ever wanted).
if (gate != null)
{
string myTag = FrameTagSafe();
foreach (string t in FrameRegistry.Tags(gate.Database))
if (!string.Equals(t, myTag, System.StringComparison.OrdinalIgnoreCase))
{
if (!Dialogs.Confirm("This drawing already holds frame " + t
+ " -- one frame per drawing is the convention, and Draw would add frame "
+ myTag + " BESIDE it (Draw only replaces its own frame). Continue anyway?"))
return;
break;
}
}
FrameGraph g = KingPostBentGraph.Build(_spec);
// Emit MANAGED timbers (editable) instead of legacy solids. Place through the current UCS
// (graph coords interpreted in the UCS -> WCS) so the frame honors the user's UCS origin +
// orientation, instead of always landing at the world origin.
var doc = AcadApp.DocumentManager.MdiActiveDocument;
Matrix3d placement = doc != null ? doc.Editor.CurrentUserCoordinateSystem : Matrix3d.Identity;
placement = ManagedFrameEmitter.Compose(placement); // stand the frame up Z-up (model basis)
// Draw/redraw: clear only THIS frame's managed timbers (by FrameTag) before emitting, so
// other managed frames + non-managed solids stay. Each emitted timber is stamped with the
// frame tag (+ per-edge bent/bay tags from the graph walk) -- the grouping layer.
string frameTag = string.IsNullOrWhiteSpace(_spec.FrameTag) ? "A" : _spec.FrameTag.Trim();
int cleared = 0;
List<JointLedgerEntry> ledger = null;
if (doc != null)
{
// The erase HARVESTS the joint ledger (recipes + geometric identities) before the
// skeleton dies; the replay below restores the joinery onto the fresh skeleton --
// hours of TJointAll / Joints-pane work survive the regen (Robert's ask).
cleared = ManagedTimber.EraseFrame(doc.Database, frameTag, out ledger);
ManagedTimber.EraseGrid(doc.Database, frameTag);
}
int drawn = ManagedFrameEmitter.Emit(g, placement, frameTag, out FrameGrid emitGrid, out int ceded);
// Structural grid: DRAWING-DERIVED so it includes any TPlace'd sub timbers assigned to this
// frame and shifts the numbering. Only floor-meeting timbers draw a line. (emitGrid is still
// used inside Emit to stamp each timber's installer label.)
FrameGrid grid = doc != null ? FrameGrid.BuildFromDrawing(doc.Database, frameTag, placement) : emitGrid;
// Free Assembly elements emit no geometry, but the RECIPE knows their stations -- fold
// them into the grid as FIRST-CLASS lines (Robert's call: same solid line, bubble, and
// sequence number as a post-backed bent; replaced the dashed provisional lines).
var freeBentZ = new List<double>();
var freeWallX = new List<double>();
_spec.FreeAssemblyStations(freeBentZ, freeWallX);
grid.MergeSpecStations(freeBentZ, freeWallX);
grid.Draw(placement, frameTag); // flat under the frame (model basis)
// Replay BEFORE the brace relabel: a replayed brace tenon changes measured Overall, and
// the size+shape group symbols should reflect the final jointed geometry. The label
// rescue (relocated members after a pure param change) arms only when the member count
// is unchanged (cleared == drawn) -- on an insert/remove, labels renumber and lie.
string replay = doc != null
? ManagedCommands.ReplayJoints(doc.Database, ledger, cleared == drawn) : null;
if (doc != null) ManagedCommands.RelabelBraces(doc.Database); // brace symbols (*, **) by size+shape
Persist();
// Stamp the emitted recipe into the frame's registry record (batch-3 #3), so re-opening
// this DRAWING refills the tree with THIS frame -- the spec follows the drawing, not the
// machine. Other record fields (the freeze gate, the TRoughIn seed) are preserved.
if (doc != null)
{
try
{
FrameRecord rec = FrameRegistry.Load(doc.Database, frameTag) ?? new FrameRecord();
rec.SpecJson = FrameSpecStore.ToJson(_spec);
FrameRegistry.Save(doc.Database, frameTag, rec);
}
catch (System.Exception ex)
{ Diag.Warn("FrameTree.Draw", "spec stamp failed (recall-on-open unavailable): " + ex.Message); }
}
doc?.Editor.WriteMessage(
"\nTDraw: frame " + frameTag + " -- cleared " + cleared + " managed timber(s); emitted "
+ drawn + " managed timbers across " + g.Nodes.Count + " nodes (grid "
+ grid.ColX.Count + " cols x " + grid.BentZ.Count + " bents)."
+ (ceded > 0 ? " " + ceded + " slot(s) held by pinned (shape-edited) members." : "")
+ (replay != null ? " " + replay : ""));
}
// Start a fresh frame project: an UN-SEEDED empty tree (Robert's call) -- grow it with
// right-click Add Bent / Add Wall, or Load a .framespec. Confirms first when it would
// discard work (Save first to keep it).
private void NewFrame()
{
if (!SpecIsEmpty && !Dialogs.Confirm(
"Start a new frame project? The current frame will be cleared (Save first if you want to keep it)."))
return;