pi-revit 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +388 -351
- package/README.md +426 -22
- package/extensions/pi-revit/index.ts +246 -187
- package/extensions/pi-revit/instance-router.ts +86 -0
- package/extensions/pi-revit/script-library.ts +146 -0
- package/extensions/pi-revit/tool-catalog.ts +67 -0
- package/package.json +59 -59
- package/skills/pi-revit/SKILL.md +196 -32
- package/skills/pi-revit/references/model-audit-export.md +29 -0
- package/skills/pi-revit/references/room-documentation.md +28 -0
- package/src/Revit/BridgeServer.cs +87 -37
- package/src/Revit/OperationStore.cs +178 -0
- package/src/Revit/ToolRegistry.cs +57 -35
- package/src/Revit/Tools/CaptureView.cs +2 -1
- package/src/Revit/Tools/ChangeElementTypes.cs +60 -0
- package/src/Revit/Tools/CreateTags.cs +95 -0
- package/src/Revit/Tools/DeleteElements.cs +44 -0
- package/src/Revit/Tools/DocumentGuard.cs +64 -64
- package/src/Revit/Tools/ElementQueryScope.cs +27 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +45 -35
- package/src/Revit/Tools/ExportDocuments.cs +121 -120
- package/src/Revit/Tools/FailureGuard.cs +26 -26
- package/src/Revit/Tools/GetElementDetails.cs +42 -12
- package/src/Revit/Tools/GetElementRelationships.cs +76 -0
- package/src/Revit/Tools/GetElements.cs +37 -23
- package/src/Revit/Tools/GetLinkedElements.cs +82 -0
- package/src/Revit/Tools/GetLinkedModels.cs +66 -0
- package/src/Revit/Tools/GetModelCoordinates.cs +49 -0
- package/src/Revit/Tools/GetModelOverview.cs +2 -2
- package/src/Revit/Tools/GetScheduleFields.cs +37 -0
- package/src/Revit/Tools/GetSchedules.cs +89 -0
- package/src/Revit/Tools/ManageElementSets.cs +106 -0
- package/src/Revit/Tools/ManageSchedules.cs +164 -0
- package/src/Revit/Tools/ManageSelection.cs +37 -36
- package/src/Revit/Tools/ManageSheetPlacements.cs +97 -0
- package/src/Revit/Tools/ManageSheets.cs +63 -0
- package/src/Revit/Tools/ManageViews.cs +100 -0
- package/src/Revit/Tools/MeasureGeometry.cs +54 -0
- package/src/Revit/Tools/ModelEditBatch.cs +102 -0
- package/src/Revit/Tools/ModelEditInputs.cs +49 -0
- package/src/Revit/Tools/OpenView.cs +2 -1
- package/src/Revit/Tools/QuerySpatialElements.cs +63 -0
- package/src/Revit/Tools/SetParameters.cs +43 -93
- package/src/Revit/Tools/SpatialBounds.cs +30 -0
- package/src/Revit/Tools/SummarizeElements.cs +87 -0
- package/src/Revit/Tools/ToolSupport.cs +1 -1
- package/src/Revit/Tools/TransformElements.cs +58 -0
- package/workspace/AGENTS.md +45 -45
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using System.Text.Json.Nodes;
|
|
3
|
+
using Autodesk.Revit.DB;
|
|
4
|
+
|
|
5
|
+
namespace RevitBridge.Tools;
|
|
6
|
+
|
|
7
|
+
internal sealed class ManageElementSets : ITool
|
|
8
|
+
{
|
|
9
|
+
private sealed record Member(long Id, string UniqueId);
|
|
10
|
+
private sealed record Snapshot(string Id, string DocumentId, string Label, DateTimeOffset Expires, IReadOnlyList<Member> Members);
|
|
11
|
+
private static readonly Dictionary<string, Snapshot> Sets = new(StringComparer.Ordinal);
|
|
12
|
+
private const int MaxElements = 10000;
|
|
13
|
+
private const int MaxSets = 32;
|
|
14
|
+
|
|
15
|
+
public string Name => "manage_element_sets";
|
|
16
|
+
public string Label => "Manage Element Sets";
|
|
17
|
+
public string Tier => "advanced";
|
|
18
|
+
public IReadOnlyList<string> Effects => new[] { "session" };
|
|
19
|
+
public string Description => "Create, list, read or forget reusable element sets without changing the model or selection. A set captures matching host-element identities (up to 10,000), expires after 30 minutes, and is valid only for the exact open document and bridge session. Read pages report live values, deleted members and identity mismatches; membership is a snapshot and filters are not re-evaluated. At most 32 sets are retained. Use read IDs with ordinary tools after checking missing members.";
|
|
20
|
+
public object ParametersSchema => new
|
|
21
|
+
{
|
|
22
|
+
type = "object",
|
|
23
|
+
properties = new
|
|
24
|
+
{
|
|
25
|
+
action = new { type = "string", @enum = new[] { "create", "list", "read", "forget" } },
|
|
26
|
+
query = ElementQueryScope.Schema(),
|
|
27
|
+
label = new { type = "string", maxLength = 160 },
|
|
28
|
+
set_id = new { type = "string" },
|
|
29
|
+
offset = new { type = "integer", minimum = 0 },
|
|
30
|
+
limit = new { type = "integer", minimum = 1, maximum = 1000 },
|
|
31
|
+
parameter_names = new { type = "array", maxItems = 20, items = new { type = "string" } },
|
|
32
|
+
include_type_parameters = new { type = "boolean" },
|
|
33
|
+
},
|
|
34
|
+
required = new[] { "action" },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
public object Execute(JsonElement args, ToolContext context)
|
|
38
|
+
{
|
|
39
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
40
|
+
string documentId = DocumentGuard.GetIdentity(doc);
|
|
41
|
+
var now = DateTimeOffset.UtcNow;
|
|
42
|
+
foreach (var expired in Sets.Where(pair => pair.Value.Expires <= now).Select(pair => pair.Key).ToArray()) Sets.Remove(expired);
|
|
43
|
+
string action = JsonArgs.GetString(args, "action") ?? "";
|
|
44
|
+
if (action == "list") return new
|
|
45
|
+
{
|
|
46
|
+
sets = Sets.Values.Where(set => set.DocumentId == documentId).OrderBy(set => set.Expires)
|
|
47
|
+
.Select(set => new { set_id = set.Id, label = set.Label, total_count = set.Members.Count, expires_at = set.Expires }).ToArray(),
|
|
48
|
+
};
|
|
49
|
+
if (action == "create")
|
|
50
|
+
{
|
|
51
|
+
if (Sets.Count >= MaxSets) throw new ArgumentException("Element-set capacity reached. Forget an existing set or wait for expiry.");
|
|
52
|
+
string label = JsonArgs.GetString(args, "label") ?? "Element set";
|
|
53
|
+
if (label.Length > 160) throw new ArgumentException("label must be at most 160 characters.");
|
|
54
|
+
var query = ElementQueryScope.Parse(args);
|
|
55
|
+
query["fields"] = new JsonArray("id"); query["count_only"] = true;
|
|
56
|
+
JsonElement Query() => JsonSerializer.SerializeToElement(((ToolOutput)new GetElements().Execute(JsonSerializer.SerializeToElement(query), context)!).Payload);
|
|
57
|
+
var count = Query();
|
|
58
|
+
int total = count.GetProperty("total_count").GetInt32();
|
|
59
|
+
if (total > MaxElements) throw new ArgumentException($"Query matches {total} elements; narrow the scope to at most {MaxElements}.");
|
|
60
|
+
query["count_only"] = false; query["limit"] = 1000;
|
|
61
|
+
var members = new List<Member>(total);
|
|
62
|
+
for (int offset = 0; offset < total; offset += 1000)
|
|
63
|
+
{
|
|
64
|
+
query["offset"] = offset;
|
|
65
|
+
foreach (var row in Query().GetProperty("elements").EnumerateArray())
|
|
66
|
+
{
|
|
67
|
+
var element = doc.GetElement(new ElementId(row.GetProperty("id").GetInt64()));
|
|
68
|
+
members.Add(new Member(element.Id.Value, element.UniqueId));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
var snapshot = new Snapshot(Guid.NewGuid().ToString("N"), documentId, label, now.AddMinutes(30), members);
|
|
72
|
+
Sets.Add(snapshot.Id, snapshot);
|
|
73
|
+
return new { set_id = snapshot.Id, document_id = documentId, label, total_count = total, expires_at = snapshot.Expires,
|
|
74
|
+
warnings = count.TryGetProperty("warnings", out var warnings) ? (object)warnings.Clone() : null };
|
|
75
|
+
}
|
|
76
|
+
if (action is not ("read" or "forget")) throw new ArgumentException("Unknown action; use create, list, read or forget.");
|
|
77
|
+
string setId = JsonArgs.GetString(args, "set_id") ?? "";
|
|
78
|
+
if (!Sets.TryGetValue(setId, out var saved)) throw new ArgumentException("Unknown or expired set_id. Create a fresh set; IDs do not survive a bridge restart.");
|
|
79
|
+
if (saved.DocumentId != documentId) throw new ArgumentException("This element set belongs to a different open document. Activate its original document or create a fresh set.");
|
|
80
|
+
if (action == "forget") { Sets.Remove(setId); return new { set_id = setId, forgotten = true }; }
|
|
81
|
+
int start = Math.Max(0, JsonArgs.GetInt(args, "offset", 0));
|
|
82
|
+
int limit = Math.Clamp(JsonArgs.GetInt(args, "limit", 100), 1, 1000);
|
|
83
|
+
var names = JsonArgs.GetStringArray(args, "parameter_names");
|
|
84
|
+
if (names?.Count > 20) throw new ArgumentException("parameter_names allows at most 20 identities.");
|
|
85
|
+
bool includeType = JsonArgs.GetBool(args, "include_type_parameters", false);
|
|
86
|
+
var page = saved.Members.Skip(start).Take(limit).ToArray();
|
|
87
|
+
var rows = new List<object>();
|
|
88
|
+
var missing = new List<object>();
|
|
89
|
+
foreach (var member in page)
|
|
90
|
+
{
|
|
91
|
+
var element = doc.GetElement(new ElementId(member.Id));
|
|
92
|
+
if (element == null || element.UniqueId != member.UniqueId)
|
|
93
|
+
{
|
|
94
|
+
missing.Add(new { id = member.Id, unique_id = member.UniqueId, reason = element == null ? "deleted" : "identity_changed" });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
var row = ElementIdentity.Build(doc, element, ElementIdentity.Fields);
|
|
98
|
+
row["unique_id"] = member.UniqueId;
|
|
99
|
+
if (names is { Count: > 0 }) row["parameters"] = GetElementDetails.ProjectParameters(doc, element, names, includeType);
|
|
100
|
+
rows.Add(row);
|
|
101
|
+
}
|
|
102
|
+
return new { set_id = setId, document_id = documentId, snapshot_count = saved.Members.Count, expires_at = saved.Expires,
|
|
103
|
+
offset = start, visited_count = page.Length, returned_count = rows.Count,
|
|
104
|
+
next_offset = start + page.Length < saved.Members.Count ? (int?)(start + page.Length) : null, elements = rows, missing };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools;
|
|
5
|
+
|
|
6
|
+
internal sealed class ManageSchedules : ITool
|
|
7
|
+
{
|
|
8
|
+
public string Name => "manage_schedules";
|
|
9
|
+
public string Label => "Manage Schedules";
|
|
10
|
+
public string Tier => "advanced";
|
|
11
|
+
public bool Write => true;
|
|
12
|
+
public string Description => "Create or configure a regular schedule in one atomic step. Create requires category and name; optional area_scheme_id supports area schedules. Configure requires schedule_id. Discover add_fields using get_schedule_fields and pass the returned parameter_id + field_type pair, including Count when listed. Count also supports field_type Count without a parameter_id. update_fields, sort_fields and filters use schedule-local field_id from get_schedules. Supplied sort_fields/filters replace their entire lists; empty arrays clear them. Numeric measured filter values require a compatible unit; returned numeric filter values use internal units. Column widths require explicit length units and apply to both grid and sheet. preview=true commit-validates then rolls back; created schedule/field IDs in previews are temporary. Revision schedules, templates, embedded schedules and calculated/combined-field authoring are outside this tool.";
|
|
13
|
+
public object ParametersSchema => new
|
|
14
|
+
{
|
|
15
|
+
type = "object", properties = new
|
|
16
|
+
{
|
|
17
|
+
action = new { type = "string", @enum = new[] { "create", "configure" } },
|
|
18
|
+
schedule_id = new { type = "integer", minimum = 1 }, category = new { type = "string" }, name = new { type = "string", minLength = 1 }, area_scheme_id = new { type = "integer", minimum = 1 },
|
|
19
|
+
is_itemized = new { type = "boolean" }, preview = ModelEditInputs.PreviewSchema,
|
|
20
|
+
add_fields = new { type = "array", maxItems = 50, items = new { type = "object", properties = new
|
|
21
|
+
{
|
|
22
|
+
parameter_id = new { type = "integer", description = "Use the ID returned by get_schedule_fields, including negative built-in IDs. Optional for Count; when supplied, the pair must be eligible for the schedule." }, field_type = new { type = "string" },
|
|
23
|
+
heading = new { type = "string" }, hidden = new { type = "boolean" }, width = new { type = "number", exclusiveMinimum = 0 }, unit = ModelEditInputs.LengthUnitSchema,
|
|
24
|
+
}, required = new[] { "field_type" } } },
|
|
25
|
+
update_fields = new { type = "array", maxItems = 50, items = new { type = "object", properties = new
|
|
26
|
+
{
|
|
27
|
+
field_id = new { type = "integer", minimum = 0 }, heading = new { type = "string" }, hidden = new { type = "boolean" }, width = new { type = "number", exclusiveMinimum = 0 }, unit = ModelEditInputs.LengthUnitSchema,
|
|
28
|
+
}, required = new[] { "field_id" } } },
|
|
29
|
+
sort_fields = new { type = "array", maxItems = 4, items = new { type = "object", properties = new { field_id = new { type = "integer", minimum = 0 }, descending = new { type = "boolean" }, show_header = new { type = "boolean" } }, required = new[] { "field_id" } } },
|
|
30
|
+
filters = new { type = "array", maxItems = 8, items = new { type = "object", properties = new
|
|
31
|
+
{
|
|
32
|
+
field_id = new { type = "integer", minimum = 0 }, comparison = new { type = "string", @enum = new[] { "equals", "not_equals", "contains", "greater_than", "less_than" } },
|
|
33
|
+
value_type = new { type = "string", @enum = new[] { "string", "number", "integer", "element_id" } }, value = new { description = "Filter value matching value_type." }, unit = new { type = "string", description = "Required for measured numbers; compatible forge unit name, e.g. meters, squareMeters, degrees." },
|
|
34
|
+
}, required = new[] { "field_id", "comparison", "value_type", "value" } } },
|
|
35
|
+
}, required = new[] { "action" },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
public object Execute(JsonElement args, ToolContext context)
|
|
39
|
+
{
|
|
40
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
41
|
+
string action = JsonArgs.GetString(args, "action") ?? "";
|
|
42
|
+
if (action is not ("create" or "configure")) throw new ArgumentException("action must be create or configure.");
|
|
43
|
+
var additions = Array(args, "add_fields", 50); var updates = Array(args, "update_fields", 50);
|
|
44
|
+
var sorts = Array(args, "sort_fields", 4); var filters = Array(args, "filters", 8);
|
|
45
|
+
return ModelEditBatch.Run(doc, Name, args, new[] { new ModelEditBatch.Step(new() { ["action"] = action }, () =>
|
|
46
|
+
{
|
|
47
|
+
ViewSchedule schedule;
|
|
48
|
+
if (action == "create")
|
|
49
|
+
{
|
|
50
|
+
string category = JsonArgs.GetString(args, "category") ?? throw new ArgumentException("category is required for creation.");
|
|
51
|
+
var categoryId = CategoryResolver.Resolve(doc, category);
|
|
52
|
+
schedule = JsonArgs.GetLong(args, "area_scheme_id") is long area ? ViewSchedule.CreateSchedule(doc, categoryId, new ElementId(area)) : ViewSchedule.CreateSchedule(doc, categoryId);
|
|
53
|
+
if (!args.TryGetProperty("name", out _)) throw new ArgumentException("name is required for creation.");
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
{
|
|
57
|
+
if (args.TryGetProperty("category", out _) || args.TryGetProperty("area_scheme_id", out _)) throw new ArgumentException("category and area_scheme_id are creation-only.");
|
|
58
|
+
schedule = doc.GetElement(new ElementId(JsonArgs.GetLong(args, "schedule_id") ?? 0)) as ViewSchedule ?? throw new ArgumentException("schedule_id is not a schedule.");
|
|
59
|
+
}
|
|
60
|
+
if (schedule.IsTemplate || schedule.IsTitleblockRevisionSchedule || schedule.Definition.IsEmbedded) throw new ArgumentException("Use a regular, non-template schedule.");
|
|
61
|
+
if (args.TryGetProperty("name", out var name))
|
|
62
|
+
{
|
|
63
|
+
if (name.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(name.GetString())) throw new ArgumentException("name must be nonempty.");
|
|
64
|
+
schedule.Name = name.GetString()!;
|
|
65
|
+
}
|
|
66
|
+
var definition = schedule.Definition;
|
|
67
|
+
var added = new List<int>();
|
|
68
|
+
foreach (var input in additions)
|
|
69
|
+
{
|
|
70
|
+
string typeName = JsonArgs.GetString(input, "field_type") ?? "";
|
|
71
|
+
if (typeName.Length == 0 || !char.IsLetter(typeName[0]) || !Enum.TryParse<ScheduleFieldType>(typeName, false, out var type) || !Enum.IsDefined(type)) throw new ArgumentException("Use a field_type from get_schedule_fields, or Count.");
|
|
72
|
+
ScheduleField field;
|
|
73
|
+
if (type == ScheduleFieldType.Count && !input.TryGetProperty("parameter_id", out _))
|
|
74
|
+
{
|
|
75
|
+
field = definition.AddField(type);
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
{
|
|
79
|
+
long parameterId = JsonArgs.GetLong(input, "parameter_id") ?? throw new ArgumentException("parameter_id is required for this field.");
|
|
80
|
+
var eligible = definition.GetSchedulableFields().FirstOrDefault(f => f.ParameterId.Value == parameterId && f.FieldType == type) ?? throw new ArgumentException("That parameter_id/field_type pair is not eligible for this schedule.");
|
|
81
|
+
field = definition.AddField(eligible);
|
|
82
|
+
}
|
|
83
|
+
ConfigureField(field, input); added.Add(field.FieldId.IntegerValue);
|
|
84
|
+
}
|
|
85
|
+
var seen = new HashSet<int>();
|
|
86
|
+
foreach (var input in updates)
|
|
87
|
+
{
|
|
88
|
+
var id = FieldId(input);
|
|
89
|
+
if (!seen.Add(id.IntegerValue)) throw new ArgumentException("update_fields contains a duplicate field_id.");
|
|
90
|
+
ConfigureField(definition.GetField(id), input);
|
|
91
|
+
}
|
|
92
|
+
if (args.TryGetProperty("is_itemized", out _)) definition.IsItemized = Bool(args, "is_itemized");
|
|
93
|
+
if (args.TryGetProperty("sort_fields", out _))
|
|
94
|
+
definition.SetSortGroupFields(sorts.Select(input => new ScheduleSortGroupField(FieldId(input), Bool(input, "descending") ? ScheduleSortOrder.Descending : ScheduleSortOrder.Ascending) { ShowHeader = Bool(input, "show_header") }).ToList());
|
|
95
|
+
if (args.TryGetProperty("filters", out _)) definition.SetFilters(filters.Select(input => Filter(definition, input)).ToList());
|
|
96
|
+
doc.Regenerate();
|
|
97
|
+
return new() { ["schedule_id"] = schedule.Id.Value, ["unique_id"] = schedule.UniqueId, ["name"] = schedule.Name,
|
|
98
|
+
["created"] = action == "create", ["id_is_temporary"] = action == "create" && JsonArgs.GetBool(args, "preview", false),
|
|
99
|
+
["added_field_ids"] = added, ["added_field_ids_are_temporary"] = JsonArgs.GetBool(args, "preview", false),
|
|
100
|
+
["is_itemized"] = definition.IsItemized, ["field_count"] = definition.GetFieldCount(), ["filter_count"] = definition.GetFilterCount(), ["sort_count"] = definition.GetSortGroupFieldCount() };
|
|
101
|
+
}) }).Payload;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private static List<JsonElement> Array(JsonElement args, string name, int max)
|
|
105
|
+
{
|
|
106
|
+
if (!args.TryGetProperty(name, out var value)) return new();
|
|
107
|
+
if (value.ValueKind != JsonValueKind.Array || value.GetArrayLength() > max) throw new ArgumentException($"{name} must be an array of at most {max} entries.");
|
|
108
|
+
var items = value.EnumerateArray().ToList();
|
|
109
|
+
if (items.Any(item => item.ValueKind != JsonValueKind.Object)) throw new ArgumentException($"{name} entries must be objects.");
|
|
110
|
+
return items;
|
|
111
|
+
}
|
|
112
|
+
private static bool Bool(JsonElement args, string name)
|
|
113
|
+
{
|
|
114
|
+
if (!args.TryGetProperty(name, out var value)) return false;
|
|
115
|
+
if (value.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) throw new ArgumentException($"{name} must be boolean.");
|
|
116
|
+
return value.GetBoolean();
|
|
117
|
+
}
|
|
118
|
+
private static ScheduleFieldId FieldId(JsonElement input)
|
|
119
|
+
{
|
|
120
|
+
long id = JsonArgs.GetLong(input, "field_id") ?? -1;
|
|
121
|
+
return id is >= 0 and <= int.MaxValue ? new ScheduleFieldId((int)id) : throw new ArgumentException("field_id must be a nonnegative schedule-local ID.");
|
|
122
|
+
}
|
|
123
|
+
private static void ConfigureField(ScheduleField field, JsonElement input)
|
|
124
|
+
{
|
|
125
|
+
if (input.TryGetProperty("heading", out var heading)) field.ColumnHeading = heading.ValueKind == JsonValueKind.String ? heading.GetString()! : throw new ArgumentException("heading must be a string.");
|
|
126
|
+
if (input.TryGetProperty("hidden", out _)) field.IsHidden = Bool(input, "hidden");
|
|
127
|
+
if (input.TryGetProperty("width", out _))
|
|
128
|
+
{
|
|
129
|
+
double width = ModelEditInputs.Number(input, "width") * ModelEditInputs.LengthScale(input);
|
|
130
|
+
if (width <= 0) throw new ArgumentException("Column width must be positive.");
|
|
131
|
+
field.GridColumnWidth = width; field.SheetColumnWidth = width;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
private static ScheduleFilter Filter(ScheduleDefinition definition, JsonElement input)
|
|
135
|
+
{
|
|
136
|
+
var id = FieldId(input); var field = definition.GetField(id);
|
|
137
|
+
var comparison = JsonArgs.GetString(input, "comparison") switch
|
|
138
|
+
{
|
|
139
|
+
"equals" => ScheduleFilterType.Equal, "not_equals" => ScheduleFilterType.NotEqual, "contains" => ScheduleFilterType.Contains,
|
|
140
|
+
"greater_than" => ScheduleFilterType.GreaterThan, "less_than" => ScheduleFilterType.LessThan, _ => throw new ArgumentException("Unknown comparison."),
|
|
141
|
+
};
|
|
142
|
+
if (comparison == ScheduleFilterType.Contains ? !definition.CanFilterBySubstring(id) : !definition.CanFilterByValue(id)) throw new ArgumentException("This field does not support that filter operation.");
|
|
143
|
+
if (!input.TryGetProperty("value", out var value)) throw new ArgumentException("Filter value is required.");
|
|
144
|
+
string? type = JsonArgs.GetString(input, "value_type");
|
|
145
|
+
if (type == "string" && value.ValueKind == JsonValueKind.String) return new ScheduleFilter(id, comparison, value.GetString()!);
|
|
146
|
+
if (type == "integer" && value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out int integer)) return new ScheduleFilter(id, comparison, integer);
|
|
147
|
+
if (type == "element_id" && value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out long elementId)) return new ScheduleFilter(id, comparison, new ElementId(elementId));
|
|
148
|
+
if (type == "number")
|
|
149
|
+
{
|
|
150
|
+
double number = ModelEditInputs.Number(input, "value");
|
|
151
|
+
var spec = field.GetSpecTypeId(); string? unit = JsonArgs.GetString(input, "unit");
|
|
152
|
+
if (UnitUtils.IsMeasurableSpec(spec))
|
|
153
|
+
{
|
|
154
|
+
if (string.IsNullOrWhiteSpace(unit)) throw new ArgumentException("Measured numeric filters require an explicit unit.");
|
|
155
|
+
var unitId = UnitResolver.Resolve(unit);
|
|
156
|
+
if (!UnitUtils.IsValidUnit(spec, unitId)) throw new ArgumentException("Filter unit does not match the field's specification.");
|
|
157
|
+
number = UnitUtils.ConvertToInternalUnits(number, unitId);
|
|
158
|
+
}
|
|
159
|
+
else if (unit != null) throw new ArgumentException("This numeric field has no measurable unit.");
|
|
160
|
+
return new ScheduleFilter(id, comparison, number);
|
|
161
|
+
}
|
|
162
|
+
throw new ArgumentException("Filter value does not match value_type.");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -11,7 +11,8 @@ namespace RevitBridge.Tools
|
|
|
11
11
|
/// </summary>
|
|
12
12
|
internal sealed class ManageSelection : ITool
|
|
13
13
|
{
|
|
14
|
-
public string Name => "manage_selection";
|
|
14
|
+
public string Name => "manage_selection";
|
|
15
|
+
public IReadOnlyList<string> Effects => new[] { "ui", "model" };
|
|
15
16
|
public string Label => "Manage Selection";
|
|
16
17
|
public string Description => "Manage the Revit selection and view focus. action 'get' returns the currently selected elements (id, name, category, typeName, levelId); 'set'/'add'/'remove' change the selection with element_ids; 'clear' empties it; 'zoom' zooms open views to element_ids (default: the current selection). isolate_in_view=true additionally applies Revit's Temporary Hide/Isolate to the affected elements in the active view — temporary view state, not a model change (Revit needs a brief internal transaction to toggle it); with an empty target set (action 'clear', or 'get' with nothing selected) it resets the temporary isolate instead. Selection and zoom never modify model data.";
|
|
17
18
|
|
|
@@ -136,25 +137,25 @@ namespace RevitBridge.Tools
|
|
|
136
137
|
compactParts.Add($"ids not found: {string.Join(", ", notFound)}");
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
if (isolateInView)
|
|
140
|
-
{
|
|
141
|
-
var view = uiDocument.ActiveGraphicalView;
|
|
142
|
-
IReadOnlyList<string> warnings;
|
|
143
|
-
try
|
|
144
|
-
{
|
|
145
|
-
if (view is null)
|
|
146
|
-
throw new ArgumentException("isolate_in_view requires an active graphical view in Revit.");
|
|
147
|
-
warnings = ApplyTemporaryIsolate(doc, view, isolateTargets);
|
|
148
|
-
}
|
|
149
|
-
catch (Exception ex)
|
|
150
|
-
{
|
|
151
|
-
throw new InvalidOperationException($"Selection action '{action}' completed before temporary isolate failed: {ex.Message}. Selection/zoom changes were not rolled back.", ex);
|
|
152
|
-
}
|
|
153
|
-
if (warnings.Count > 0)
|
|
154
|
-
{
|
|
155
|
-
payload["commitWarnings"] = warnings;
|
|
156
|
-
compactParts.Add($"{warnings.Count} Revit warning(s) auto-dismissed (see commitWarnings)");
|
|
157
|
-
}
|
|
140
|
+
if (isolateInView)
|
|
141
|
+
{
|
|
142
|
+
var view = uiDocument.ActiveGraphicalView;
|
|
143
|
+
IReadOnlyList<string> warnings;
|
|
144
|
+
try
|
|
145
|
+
{
|
|
146
|
+
if (view is null)
|
|
147
|
+
throw new ArgumentException("isolate_in_view requires an active graphical view in Revit.");
|
|
148
|
+
warnings = ApplyTemporaryIsolate(doc, view, isolateTargets);
|
|
149
|
+
}
|
|
150
|
+
catch (Exception ex)
|
|
151
|
+
{
|
|
152
|
+
throw new InvalidOperationException($"Selection action '{action}' completed before temporary isolate failed: {ex.Message}. Selection/zoom changes were not rolled back.", ex);
|
|
153
|
+
}
|
|
154
|
+
if (warnings.Count > 0)
|
|
155
|
+
{
|
|
156
|
+
payload["commitWarnings"] = warnings;
|
|
157
|
+
compactParts.Add($"{warnings.Count} Revit warning(s) auto-dismissed (see commitWarnings)");
|
|
158
|
+
}
|
|
158
159
|
payload["viewId"] = view.Id.Value;
|
|
159
160
|
payload["viewName"] = view.Name;
|
|
160
161
|
if (isolateTargets.Count == 0)
|
|
@@ -203,28 +204,28 @@ namespace RevitBridge.Tools
|
|
|
203
204
|
/// only changes it inside an open transaction — so exactly this branch wraps
|
|
204
205
|
/// a small one while the tool stays Write = false.
|
|
205
206
|
/// </summary>
|
|
206
|
-
private static IReadOnlyList<string> ApplyTemporaryIsolate(Document doc, View view, ICollection<ElementId> elementIds)
|
|
207
|
-
{
|
|
208
|
-
using var transaction = new Transaction(doc, "manage_selection: temporary isolate");
|
|
209
|
-
if (transaction.Start() != TransactionStatus.Started)
|
|
210
|
-
throw new InvalidOperationException("Unable to start the temporary-isolate transaction.");
|
|
211
|
-
var failureGuard = FailureGuard.Attach(transaction);
|
|
207
|
+
private static IReadOnlyList<string> ApplyTemporaryIsolate(Document doc, View view, ICollection<ElementId> elementIds)
|
|
208
|
+
{
|
|
209
|
+
using var transaction = new Transaction(doc, "manage_selection: temporary isolate");
|
|
210
|
+
if (transaction.Start() != TransactionStatus.Started)
|
|
211
|
+
throw new InvalidOperationException("Unable to start the temporary-isolate transaction.");
|
|
212
|
+
var failureGuard = FailureGuard.Attach(transaction);
|
|
212
213
|
try
|
|
213
214
|
{
|
|
214
215
|
if (elementIds.Count == 0)
|
|
215
216
|
view.DisableTemporaryViewMode(TemporaryViewMode.TemporaryHideIsolate);
|
|
216
217
|
else
|
|
217
218
|
view.IsolateElementsTemporary(elementIds);
|
|
218
|
-
var status = transaction.Commit();
|
|
219
|
-
var finalStatus = transaction.GetStatus();
|
|
220
|
-
if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
|
|
221
|
-
throw new InvalidOperationException($"The temporary-isolate commit returned {status}; current transaction status is {finalStatus}." + failureGuard.DescribeErrors());
|
|
222
|
-
return failureGuard.Warnings;
|
|
223
|
-
}
|
|
224
|
-
catch (Exception ex)
|
|
225
|
-
{
|
|
226
|
-
throw new InvalidOperationException($"Temporary isolate in view '{view.Name}' failed: {ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}", ex);
|
|
227
|
-
}
|
|
219
|
+
var status = transaction.Commit();
|
|
220
|
+
var finalStatus = transaction.GetStatus();
|
|
221
|
+
if (status != TransactionStatus.Committed || finalStatus != TransactionStatus.Committed)
|
|
222
|
+
throw new InvalidOperationException($"The temporary-isolate commit returned {status}; current transaction status is {finalStatus}." + failureGuard.DescribeErrors());
|
|
223
|
+
return failureGuard.Warnings;
|
|
224
|
+
}
|
|
225
|
+
catch (Exception ex)
|
|
226
|
+
{
|
|
227
|
+
throw new InvalidOperationException($"Temporary isolate in view '{view.Name}' failed: {ex.Message} {FailureGuard.RollBackAndDescribe(transaction)}", ex);
|
|
228
|
+
}
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools;
|
|
5
|
+
|
|
6
|
+
internal sealed class ManageSheetPlacements : ITool
|
|
7
|
+
{
|
|
8
|
+
public string Name => "manage_sheet_placements";
|
|
9
|
+
public string Label => "Manage Sheet Placements";
|
|
10
|
+
public string Tier => "advanced";
|
|
11
|
+
public bool Write => true;
|
|
12
|
+
public string Description => "List, place or move viewports and schedule instances on a sheet. Positions are paper-space sheet coordinates with explicit length units, [x,y,0]; never multiply by view scale. For viewports position is the box center excluding its label; for schedules it is the insertion point. Place requires sheet_id/view_id, move requires placement_id. Viewports may rotate none/clockwise/counterclockwise; schedule rotation is not supported. preview=true commit-validates then rolls back; new placement IDs are temporary. List includes both viewports and schedules with paging. Exact document identity is required for every action.";
|
|
13
|
+
public object ParametersSchema => new
|
|
14
|
+
{
|
|
15
|
+
type = "object", properties = new
|
|
16
|
+
{
|
|
17
|
+
action = new { type = "string", @enum = new[] { "list", "place", "move" } },
|
|
18
|
+
sheet_id = new { type = "integer", minimum = 1 }, view_id = new { type = "integer", minimum = 1 }, placement_id = new { type = "integer", minimum = 1 },
|
|
19
|
+
position = ModelEditInputs.VectorSchema("Sheet paper-space [x,y,0] in unit."), unit = ModelEditInputs.LengthUnitSchema,
|
|
20
|
+
rotation = new { type = "string", @enum = new[] { "none", "clockwise", "counterclockwise" } },
|
|
21
|
+
offset = new { type = "integer", minimum = 0 }, limit = new { type = "integer", minimum = 1, maximum = 200 }, preview = ModelEditInputs.PreviewSchema,
|
|
22
|
+
}, required = new[] { "action" },
|
|
23
|
+
};
|
|
24
|
+
public object Execute(JsonElement args, ToolContext context)
|
|
25
|
+
{
|
|
26
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
27
|
+
string action = JsonArgs.GetString(args, "action") ?? "";
|
|
28
|
+
ElementId Id(string name) => new(JsonArgs.GetLong(args, name) is long id && id > 0 ? id : throw new ArgumentException($"{name} must be a positive ID."));
|
|
29
|
+
if (action == "list")
|
|
30
|
+
{
|
|
31
|
+
var sheet = doc.GetElement(Id("sheet_id")) as ViewSheet ?? throw new ArgumentException("sheet_id is not a sheet.");
|
|
32
|
+
var items = new FilteredElementCollector(doc).OwnedByView(sheet.Id).WhereElementIsNotElementType()
|
|
33
|
+
.Where(e => e is Viewport or ScheduleSheetInstance).OrderBy(e => e.Id.Value).ToArray();
|
|
34
|
+
int offset = JsonArgs.GetInt(args, "offset", 0), limit = JsonArgs.GetInt(args, "limit", 100);
|
|
35
|
+
if (offset < 0 || limit is < 1 or > 200) throw new ArgumentException("Invalid paging range.");
|
|
36
|
+
var page = items.Skip(offset).Take(limit).Select(Describe).ToArray();
|
|
37
|
+
return new { sheet_id = sheet.Id.Value, total_count = items.Length, offset, returned_count = page.Length, next_offset = offset + page.Length < items.Length ? (int?)(offset + page.Length) : null, placements = page, coordinate_system = "sheet_paper", unit = "feet" };
|
|
38
|
+
}
|
|
39
|
+
if (action is not ("place" or "move")) throw new ArgumentException("action must be list, place or move.");
|
|
40
|
+
var point = ModelEditInputs.Vector(args, "position").Multiply(ModelEditInputs.LengthScale(args));
|
|
41
|
+
if (Math.Abs(point.Z) > 1e-9) throw new ArgumentException("Sheet position Z must be zero.");
|
|
42
|
+
var rotation = JsonArgs.GetString(args, "rotation") switch
|
|
43
|
+
{
|
|
44
|
+
null => (ViewportRotation?)null, "none" => ViewportRotation.None, "clockwise" => ViewportRotation.Clockwise,
|
|
45
|
+
"counterclockwise" => ViewportRotation.Counterclockwise, _ => throw new ArgumentException("Invalid rotation."),
|
|
46
|
+
};
|
|
47
|
+
return ModelEditBatch.Run(doc, Name, args, new[] { new ModelEditBatch.Step(new() { ["action"] = action }, () =>
|
|
48
|
+
{
|
|
49
|
+
Element placement;
|
|
50
|
+
object? before = null;
|
|
51
|
+
if (action == "place")
|
|
52
|
+
{
|
|
53
|
+
var sheet = doc.GetElement(Id("sheet_id")) as ViewSheet ?? throw new ArgumentException("sheet_id is not a sheet.");
|
|
54
|
+
if (sheet.IsPlaceholder) throw new ArgumentException("Cannot place content on a placeholder sheet.");
|
|
55
|
+
var view = doc.GetElement(Id("view_id")) as View ?? throw new ArgumentException("view_id is not a view or schedule.");
|
|
56
|
+
if (view is ViewSchedule schedule)
|
|
57
|
+
{
|
|
58
|
+
if (rotation != null) throw new ArgumentException("Schedules do not support viewport rotation.");
|
|
59
|
+
placement = ScheduleSheetInstance.Create(doc, sheet.Id, schedule.Id, point);
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
{
|
|
63
|
+
if (!Viewport.CanAddViewToSheet(doc, sheet.Id, view.Id)) throw new ArgumentException("This view cannot be added to that sheet; it may already be placed.");
|
|
64
|
+
placement = Viewport.Create(doc, sheet.Id, view.Id, point);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
{
|
|
69
|
+
placement = doc.GetElement(Id("placement_id")) ?? throw new ArgumentException("placement_id was not found.");
|
|
70
|
+
if (args.TryGetProperty("sheet_id", out _) && placement.OwnerViewId != Id("sheet_id")) throw new ArgumentException("The placement does not belong to the expected sheet_id.");
|
|
71
|
+
if (placement.Pinned) throw new ArgumentException("Placement is pinned; it was not unpinned.");
|
|
72
|
+
before = Describe(placement);
|
|
73
|
+
if (placement is Viewport viewport) viewport.SetBoxCenter(point);
|
|
74
|
+
else if (placement is ScheduleSheetInstance schedule)
|
|
75
|
+
{
|
|
76
|
+
if (rotation != null) throw new ArgumentException("Schedules do not support viewport rotation.");
|
|
77
|
+
schedule.Point = point;
|
|
78
|
+
}
|
|
79
|
+
else throw new ArgumentException("placement_id must identify a viewport or schedule instance.");
|
|
80
|
+
}
|
|
81
|
+
if (rotation != null && placement is Viewport target) target.Rotation = rotation.Value;
|
|
82
|
+
doc.Regenerate();
|
|
83
|
+
if (placement is Viewport positioned) { positioned.SetBoxCenter(point); doc.Regenerate(); }
|
|
84
|
+
return new() { ["before"] = before, ["placement"] = Describe(placement), ["created"] = action == "place", ["id_is_temporary"] = action == "place" && JsonArgs.GetBool(args, "preview", false) };
|
|
85
|
+
}) }).Payload;
|
|
86
|
+
}
|
|
87
|
+
private static object Describe(Element element)
|
|
88
|
+
{
|
|
89
|
+
static double[] Point(XYZ p) => new[] { p.X, p.Y, p.Z };
|
|
90
|
+
return element switch
|
|
91
|
+
{
|
|
92
|
+
Viewport v => new { id = v.Id.Value, unique_id = v.UniqueId, kind = "viewport", sheet_id = v.SheetId.Value, view_id = v.ViewId.Value, position = Point(v.GetBoxCenter()), position_kind = "box_center_excluding_label", rotation = v.Rotation.ToString(), unit = "feet" } as object,
|
|
93
|
+
ScheduleSheetInstance s => new { id = s.Id.Value, unique_id = s.UniqueId, kind = "schedule", sheet_id = s.OwnerViewId.Value, view_id = s.ScheduleId.Value, position = Point(s.Point), position_kind = "insertion_point", unit = "feet" },
|
|
94
|
+
_ => throw new ArgumentException("Element is not a sheet placement."),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools;
|
|
5
|
+
|
|
6
|
+
internal sealed class ManageSheets : ITool
|
|
7
|
+
{
|
|
8
|
+
public string Name => "manage_sheets";
|
|
9
|
+
public string Label => "Manage Sheets";
|
|
10
|
+
public string Tier => "advanced";
|
|
11
|
+
public bool Write => true;
|
|
12
|
+
public string Description => "Create or rename/renumber a drawing sheet. Creation requires name and number; optional titleblock_type_id chooses a loaded titleblock FamilySymbol, otherwise creates a sheet without a titleblock. Update requires sheet_id and accepts name/number. Revit validates unique sheet numbers and valid text; sheet names may repeat; the call rolls back on failure. preview=true commit-validates and rolls back, and new preview sheet IDs are temporary. Use manage_sheet_placements for views/schedules, get_elements for querying sheets, and delete_elements for removal.";
|
|
13
|
+
public object ParametersSchema => new
|
|
14
|
+
{
|
|
15
|
+
type = "object", properties = new
|
|
16
|
+
{
|
|
17
|
+
action = new { type = "string", @enum = new[] { "create", "update" } },
|
|
18
|
+
sheet_id = new { type = "integer", minimum = 1 }, name = new { type = "string", minLength = 1 }, number = new { type = "string", minLength = 1 },
|
|
19
|
+
titleblock_type_id = new { type = "integer", minimum = 1, description = "Creation only; omitted means no titleblock." }, preview = ModelEditInputs.PreviewSchema,
|
|
20
|
+
}, required = new[] { "action" },
|
|
21
|
+
};
|
|
22
|
+
public object Execute(JsonElement args, ToolContext context)
|
|
23
|
+
{
|
|
24
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
25
|
+
string action = JsonArgs.GetString(args, "action") ?? "";
|
|
26
|
+
if (action is not ("create" or "update")) throw new ArgumentException("action must be create or update.");
|
|
27
|
+
string? ReadText(string key)
|
|
28
|
+
{
|
|
29
|
+
if (!args.TryGetProperty(key, out var value)) return null;
|
|
30
|
+
if (value.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(value.GetString())) throw new ArgumentException($"{key} must be a nonempty string.");
|
|
31
|
+
return value.GetString();
|
|
32
|
+
}
|
|
33
|
+
string? name = ReadText("name"), number = ReadText("number");
|
|
34
|
+
if (action == "create" && (name == null || number == null)) throw new ArgumentException("Sheet creation requires name and number.");
|
|
35
|
+
if (action == "update" && args.TryGetProperty("titleblock_type_id", out _)) throw new ArgumentException("titleblock_type_id is only supported when creating a sheet; use change_element_types on an existing titleblock instance.");
|
|
36
|
+
return ModelEditBatch.Run(doc, Name, args, new[] { new ModelEditBatch.Step(new() { ["action"] = action }, () =>
|
|
37
|
+
{
|
|
38
|
+
ViewSheet sheet;
|
|
39
|
+
object? before = null;
|
|
40
|
+
if (action == "create")
|
|
41
|
+
{
|
|
42
|
+
var typeId = ElementId.InvalidElementId;
|
|
43
|
+
if (args.TryGetProperty("titleblock_type_id", out _))
|
|
44
|
+
{
|
|
45
|
+
typeId = new ElementId(JsonArgs.GetLong(args, "titleblock_type_id") ?? 0);
|
|
46
|
+
var type = doc.GetElement(typeId) as FamilySymbol;
|
|
47
|
+
if (type?.Category?.Id.Value != (long)BuiltInCategory.OST_TitleBlocks) throw new ArgumentException("titleblock_type_id must identify a titleblock FamilySymbol.");
|
|
48
|
+
}
|
|
49
|
+
sheet = ViewSheet.Create(doc, typeId);
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
{
|
|
53
|
+
sheet = doc.GetElement(new ElementId(JsonArgs.GetLong(args, "sheet_id") ?? 0)) as ViewSheet ?? throw new ArgumentException("sheet_id is not a sheet.");
|
|
54
|
+
before = Describe(sheet);
|
|
55
|
+
}
|
|
56
|
+
if (number != null) sheet.SheetNumber = number;
|
|
57
|
+
if (name != null) sheet.Name = name;
|
|
58
|
+
doc.Regenerate();
|
|
59
|
+
return new() { ["before"] = before, ["sheet"] = Describe(sheet), ["created"] = action == "create", ["id_is_temporary"] = action == "create" && JsonArgs.GetBool(args, "preview", false) };
|
|
60
|
+
}) }).Payload;
|
|
61
|
+
}
|
|
62
|
+
private static object Describe(ViewSheet sheet) => new { id = sheet.Id.Value, unique_id = sheet.UniqueId, name = sheet.Name, number = sheet.SheetNumber, placeholder = sheet.IsPlaceholder };
|
|
63
|
+
}
|