pi-revit 0.3.1 → 0.5.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/AGENTS.md +167 -0
- package/CHANGELOG.md +114 -42
- package/README.md +598 -138
- package/bin/pi-revit.js +9 -9
- package/docs/architecture.md +271 -0
- package/docs/evaluation.md +434 -0
- package/docs/invariants.json +147 -0
- package/extensions/pi-revit/completion-monitor.ts +55 -0
- package/extensions/pi-revit/contracts.ts +146 -0
- package/extensions/pi-revit/discovery.ts +93 -0
- package/extensions/pi-revit/index.ts +231 -85
- package/extensions/pi-revit/instance-router.ts +86 -0
- package/extensions/pi-revit/platform-prompt.ts +40 -0
- package/extensions/pi-revit/scope-monitor.ts +114 -0
- package/extensions/pi-revit/script-library.ts +146 -0
- package/extensions/pi-revit/tool-catalog.ts +166 -0
- package/extensions/pi-revit/tool-documentation.ts +72 -0
- package/extensions/pi-revit/tool-schema.ts +8 -0
- package/package.json +65 -59
- package/scripts/build.ps1 +9 -9
- package/scripts/check-sdk.ps1 +66 -66
- package/scripts/check-tool-documentation.mjs +287 -0
- package/scripts/deploy.ps1 +16 -16
- package/scripts/generate-contracts.mjs +80 -0
- package/scripts/lib/platform.mjs +226 -0
- package/scripts/test-extension.mjs +15 -0
- package/skills/pi-revit/SKILL.md +39 -63
- package/skills/pi-revit/contracts.generated.json +3524 -0
- package/skills/pi-revit/references/execution-rules.md +41 -0
- package/skills/pi-revit/references/model-audit-export.md +40 -0
- package/skills/pi-revit/references/operation-recovery.md +33 -0
- package/skills/pi-revit/references/room-documentation.md +39 -0
- package/skills/pi-revit/references/tool-index.md +89 -0
- package/skills/pi-revit/references/tools/capture_view.md +62 -0
- package/skills/pi-revit/references/tools/change_element_types.md +65 -0
- package/skills/pi-revit/references/tools/create_tags.md +85 -0
- package/skills/pi-revit/references/tools/delete_elements.md +66 -0
- package/skills/pi-revit/references/tools/execute_csharp.md +81 -0
- package/skills/pi-revit/references/tools/export_documents.md +75 -0
- package/skills/pi-revit/references/tools/find_revit_tools.md +96 -0
- package/skills/pi-revit/references/tools/get_element_details.md +66 -0
- package/skills/pi-revit/references/tools/get_element_relationships.md +61 -0
- package/skills/pi-revit/references/tools/get_element_types.md +67 -0
- package/skills/pi-revit/references/tools/get_elements.md +87 -0
- package/skills/pi-revit/references/tools/get_linked_elements.md +79 -0
- package/skills/pi-revit/references/tools/get_linked_models.md +57 -0
- package/skills/pi-revit/references/tools/get_model_coordinates.md +64 -0
- package/skills/pi-revit/references/tools/get_model_health.md +53 -0
- package/skills/pi-revit/references/tools/get_model_overview.md +57 -0
- package/skills/pi-revit/references/tools/get_revit_operation.md +54 -0
- package/skills/pi-revit/references/tools/get_schedule_fields.md +62 -0
- package/skills/pi-revit/references/tools/get_schedules.md +71 -0
- package/skills/pi-revit/references/tools/manage_element_sets.md +92 -0
- package/skills/pi-revit/references/tools/manage_revit_instances.md +63 -0
- package/skills/pi-revit/references/tools/manage_revit_scripts.md +109 -0
- package/skills/pi-revit/references/tools/manage_schedules.md +90 -0
- package/skills/pi-revit/references/tools/manage_selection.md +66 -0
- package/skills/pi-revit/references/tools/manage_sheet_placements.md +82 -0
- package/skills/pi-revit/references/tools/manage_sheets.md +71 -0
- package/skills/pi-revit/references/tools/manage_views.md +95 -0
- package/skills/pi-revit/references/tools/measure_geometry.md +71 -0
- package/skills/pi-revit/references/tools/open_view.md +59 -0
- package/skills/pi-revit/references/tools/ping.md +41 -0
- package/skills/pi-revit/references/tools/query_spatial_elements.md +74 -0
- package/skills/pi-revit/references/tools/read_revit_result.md +53 -0
- package/skills/pi-revit/references/tools/search_api_docs.md +65 -0
- package/skills/pi-revit/references/tools/set_parameters.md +75 -0
- package/skills/pi-revit/references/tools/summarize_elements.md +64 -0
- package/skills/pi-revit/references/tools/transform_elements.md +79 -0
- package/skills/pi-revit/references/visual-verification.md +36 -0
- package/skills/pi-revit/tool-manifest.json +338 -0
- package/src/Revit/BridgeServer.cs +75 -19
- package/src/Revit/OperationStore.cs +178 -0
- package/src/Revit/ToolRegistry.cs +61 -8
- package/src/Revit/Tools/CaptureView.cs +9 -0
- package/src/Revit/Tools/ChangeElementTypes.cs +74 -0
- package/src/Revit/Tools/ChangeSet.cs +39 -0
- package/src/Revit/Tools/CreateTags.cs +107 -0
- package/src/Revit/Tools/DeleteElements.cs +53 -0
- package/src/Revit/Tools/DocumentGuard.cs +12 -2
- package/src/Revit/Tools/ElementNames.cs +103 -0
- package/src/Revit/Tools/ElementQueryScope.cs +27 -0
- package/src/Revit/Tools/ElementTraits.cs +53 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +26 -7
- package/src/Revit/Tools/ExportDocuments.cs +9 -0
- package/src/Revit/Tools/FailureGuard.cs +26 -26
- package/src/Revit/Tools/GetElementDetails.cs +28 -2
- package/src/Revit/Tools/GetElementRelationships.cs +82 -0
- package/src/Revit/Tools/GetElementTypes.cs +8 -0
- package/src/Revit/Tools/GetElements.cs +58 -55
- package/src/Revit/Tools/GetLinkedElements.cs +89 -0
- package/src/Revit/Tools/GetLinkedModels.cs +73 -0
- package/src/Revit/Tools/GetModelCoordinates.cs +56 -0
- package/src/Revit/Tools/GetModelHealth.cs +7 -0
- package/src/Revit/Tools/GetModelOverview.cs +187 -160
- package/src/Revit/Tools/GetScheduleFields.cs +44 -0
- package/src/Revit/Tools/GetSchedules.cs +96 -0
- package/src/Revit/Tools/InheritedState.Summary.cs +57 -0
- package/src/Revit/Tools/InheritedState.cs +144 -0
- package/src/Revit/Tools/ManageElementSets.cs +114 -0
- package/src/Revit/Tools/ManageSchedules.cs +174 -0
- package/src/Revit/Tools/ManageSelection.cs +9 -0
- package/src/Revit/Tools/ManageSheetPlacements.cs +113 -0
- package/src/Revit/Tools/ManageSheets.cs +72 -0
- package/src/Revit/Tools/ManageViews.cs +115 -0
- package/src/Revit/Tools/MeasureGeometry.cs +60 -0
- package/src/Revit/Tools/ModelChanges.cs +154 -0
- package/src/Revit/Tools/ModelEditBatch.cs +105 -0
- package/src/Revit/Tools/ModelEditInputs.cs +49 -0
- package/src/Revit/Tools/OpenView.cs +8 -0
- package/src/Revit/Tools/ParameterResolver.cs +94 -0
- package/src/Revit/Tools/QuerySpatialElements.cs +70 -0
- package/src/Revit/Tools/SearchApiDocs.cs +72 -4
- package/src/Revit/Tools/SetParameters.cs +50 -119
- package/src/Revit/Tools/SpatialBounds.cs +30 -0
- package/src/Revit/Tools/SummarizeElements.cs +94 -0
- package/src/Revit/Tools/ToolContract.cs +48 -0
- package/src/Revit/Tools/ToolSupport.cs +4 -0
- package/src/Revit/Tools/TransformElements.cs +73 -0
- package/workspace/AGENTS.md +54 -48
|
@@ -34,6 +34,9 @@ namespace RevitBridge
|
|
|
34
34
|
/// <summary>True only for tools that mutate the model. Write tools own their transaction.</summary>
|
|
35
35
|
bool Write => false;
|
|
36
36
|
|
|
37
|
+
/// <summary>Potential effects, independent of whether this particular call changes anything.</summary>
|
|
38
|
+
IReadOnlyList<string> Effects => Write ? new[] { "model" } : Array.Empty<string>();
|
|
39
|
+
|
|
37
40
|
/// <summary>
|
|
38
41
|
/// False only for tools that never touch the Revit API. They skip the bridge's
|
|
39
42
|
/// no-document 409 gate and run directly on the server task instead of the Revit
|
|
@@ -44,12 +47,37 @@ namespace RevitBridge
|
|
|
44
47
|
/// <summary>Activation tier metadata: "core" (default) or "advanced".</summary>
|
|
45
48
|
string Tier => "core";
|
|
46
49
|
|
|
50
|
+
/// <summary>
|
|
51
|
+
/// Document kinds the tool works in: DocumentKind.Both or DocumentKind.ProjectOnly. Every
|
|
52
|
+
/// bridge tool declares it (the documentation gate requires the declaration); the
|
|
53
|
+
/// dispatcher refuses other kinds before the tool runs.
|
|
54
|
+
/// </summary>
|
|
55
|
+
IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
|
|
56
|
+
|
|
47
57
|
/// <summary>Optional one-line "Available tools" system prompt entry.</summary>
|
|
48
58
|
string? PromptSnippet => null;
|
|
49
59
|
|
|
50
60
|
/// <summary>Optional system prompt guideline bullets. Each bullet must name the tool.</summary>
|
|
51
61
|
IReadOnlyList<string>? PromptGuidelines => null;
|
|
52
62
|
|
|
63
|
+
/// <summary>
|
|
64
|
+
/// English discovery terms beyond the name and description: synonyms, outcome words
|
|
65
|
+
/// ("screenshot"), verbs and Revit nouns. Other UI languages are mapped onto these
|
|
66
|
+
/// terms by discovery alias files, never by translating tool metadata.
|
|
67
|
+
/// </summary>
|
|
68
|
+
IReadOnlyList<string> Keywords => Array.Empty<string>();
|
|
69
|
+
|
|
70
|
+
/// <summary>Declared capability boundaries, each with an alternative route.</summary>
|
|
71
|
+
IReadOnlyList<ToolLimit> Limits => Array.Empty<ToolLimit>();
|
|
72
|
+
|
|
73
|
+
/// <summary>
|
|
74
|
+
/// Minimum sufficient check that the requested outcome happened: "reread" (query the
|
|
75
|
+
/// changed state again), "capture" (inspect an image of the visible result),
|
|
76
|
+
/// "inspect_output" (open the produced file) or "none" (no durable outcome to check).
|
|
77
|
+
/// Required when the tool can write or has effects; null for pure reads.
|
|
78
|
+
/// </summary>
|
|
79
|
+
string? Verification => null;
|
|
80
|
+
|
|
53
81
|
/// <summary>
|
|
54
82
|
/// Runs on the Revit API thread (or the server task when RequiresDocument is false).
|
|
55
83
|
/// Returns a ToolOutput or any plain JSON-serializable payload.
|
|
@@ -77,6 +105,24 @@ namespace RevitBridge
|
|
|
77
105
|
registry.Add(new CaptureView());
|
|
78
106
|
registry.Add(new ExportDocuments());
|
|
79
107
|
registry.Add(new GetModelHealth());
|
|
108
|
+
registry.Add(new GetLinkedModels());
|
|
109
|
+
registry.Add(new GetLinkedElements());
|
|
110
|
+
registry.Add(new GetSchedules());
|
|
111
|
+
registry.Add(new GetElementRelationships());
|
|
112
|
+
registry.Add(new SummarizeElements());
|
|
113
|
+
registry.Add(new ManageElementSets());
|
|
114
|
+
registry.Add(new TransformElements());
|
|
115
|
+
registry.Add(new Tools.DeleteElements());
|
|
116
|
+
registry.Add(new ChangeElementTypes());
|
|
117
|
+
registry.Add(new ManageViews());
|
|
118
|
+
registry.Add(new ManageSheets());
|
|
119
|
+
registry.Add(new ManageSheetPlacements());
|
|
120
|
+
registry.Add(new GetScheduleFields());
|
|
121
|
+
registry.Add(new ManageSchedules());
|
|
122
|
+
registry.Add(new CreateTags());
|
|
123
|
+
registry.Add(new QuerySpatialElements());
|
|
124
|
+
registry.Add(new MeasureGeometry());
|
|
125
|
+
registry.Add(new GetModelCoordinates());
|
|
80
126
|
return registry;
|
|
81
127
|
}
|
|
82
128
|
|
|
@@ -103,14 +149,17 @@ namespace RevitBridge
|
|
|
103
149
|
parameters = DescribeParameters(tool),
|
|
104
150
|
executionMode = "sequential",
|
|
105
151
|
write = tool.Write,
|
|
152
|
+
effects = tool.Effects,
|
|
106
153
|
requiresDocument = tool.RequiresDocument,
|
|
154
|
+
documentKinds = tool.DocumentKinds,
|
|
155
|
+
keywords = tool.Keywords,
|
|
156
|
+
limits = tool.Limits.Select(limit => new { what = limit.What, alternative = new { kind = limit.Alternative, @ref = limit.Ref } }).ToArray(),
|
|
157
|
+
verification = tool.Verification,
|
|
107
158
|
promptSnippet = tool.PromptSnippet,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}).ToArray()
|
|
113
|
-
: tool.PromptGuidelines,
|
|
159
|
+
// Cross-cutting rules (document identity, manuals, capability and completion
|
|
160
|
+
// protocols) are stated once by the Pi extension's platform section, not
|
|
161
|
+
// repeated per tool; guidelines here are tool-specific.
|
|
162
|
+
promptGuidelines = tool.PromptGuidelines,
|
|
114
163
|
};
|
|
115
164
|
|
|
116
165
|
private static object DescribeParameters(ITool tool)
|
|
@@ -118,12 +167,16 @@ namespace RevitBridge
|
|
|
118
167
|
if (!tool.RequiresDocument) return tool.ParametersSchema;
|
|
119
168
|
var schema = JsonSerializer.SerializeToNode(tool.ParametersSchema)!.AsObject();
|
|
120
169
|
var properties = schema["properties"]!.AsObject();
|
|
170
|
+
bool identityRequired = tool.Write || DocumentGuard.AlwaysRequiresIdentity(tool.Name);
|
|
171
|
+
// The description must agree with requiredness in the same schema.
|
|
121
172
|
properties["expected_document_id"] = new JsonObject
|
|
122
173
|
{
|
|
123
174
|
["type"] = "string",
|
|
124
|
-
["description"] =
|
|
175
|
+
["description"] = identityRequired
|
|
176
|
+
? "Required: exact opaque project.documentId from get_model_overview for the intended open document. Invalid after close/reopen or bridge restart."
|
|
177
|
+
: "Exact opaque project.documentId from get_model_overview. Not required by the schema for reads, but required at runtime for any model change or UI change this tool performs; when supplied, the call runs only against that document. Invalid after close/reopen or bridge restart."
|
|
125
178
|
};
|
|
126
|
-
if (
|
|
179
|
+
if (identityRequired)
|
|
127
180
|
{
|
|
128
181
|
var required = schema["required"] as JsonArray ?? new JsonArray();
|
|
129
182
|
if (!required.Any(x => x?.GetValue<string>() == "expected_document_id")) required.Add("expected_document_id");
|
|
@@ -20,6 +20,15 @@ namespace RevitBridge.Tools
|
|
|
20
20
|
private static readonly TimeSpan CaptureRetention = TimeSpan.FromHours(24);
|
|
21
21
|
|
|
22
22
|
public string Name => "capture_view";
|
|
23
|
+
public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
|
|
24
|
+
public IReadOnlyList<string> Keywords => new[] { "screenshot", "image", "png", "picture", "snapshot", "visual check" };
|
|
25
|
+
public IReadOnlyList<ToolLimit> Limits => new[]
|
|
26
|
+
{
|
|
27
|
+
new ToolLimit("Image contents or visual judgement (returns a file path only)", "tool", "read (open the returned PNG)"),
|
|
28
|
+
new ToolLimit("Deliverable PDF, DWG or IFC files", "tool", "export_documents"),
|
|
29
|
+
};
|
|
30
|
+
public string? Verification => "inspect_output";
|
|
31
|
+
public IReadOnlyList<string> Effects => new[] { "files" };
|
|
23
32
|
public string Label => "Capture View";
|
|
24
33
|
public string Description => "Export a PNG snapshot of a Revit view to a temporary file and return its path — the response contains NO image data; open the returned filePath with the read tool to actually see the image. Defaults to the active view; pass view_id for any other graphical view or sheet (find ids with get_elements, category 'Views' or 'Sheets'). The long image edge is capped at 1568 px. Schedules and view templates cannot be captured.";
|
|
25
34
|
public string Tier => "advanced";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools;
|
|
5
|
+
|
|
6
|
+
internal sealed class ChangeElementTypes : ITool
|
|
7
|
+
{
|
|
8
|
+
/// <summary>Updates whose per-view graphics are scanned; each scan visits every view.</summary>
|
|
9
|
+
private const int ViewGraphicsLimit = 20;
|
|
10
|
+
public string Name => "change_element_types";
|
|
11
|
+
public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
|
|
12
|
+
public IReadOnlyList<string> Keywords => new[] { "swap type", "replace type", "change family type", "retype", "switch type" };
|
|
13
|
+
public IReadOnlyList<ToolLimit> Limits => new[]
|
|
14
|
+
{
|
|
15
|
+
new ToolLimit("Editing a type definition's parameters", "tool", "set_parameters"),
|
|
16
|
+
new ToolLimit("Creating a new type", "api", "ElementType.Duplicate"),
|
|
17
|
+
new ToolLimit("Loading a family", "api", "Document.LoadFamily"),
|
|
18
|
+
};
|
|
19
|
+
public string? Verification => "reread";
|
|
20
|
+
public string Label => "Change Element Types";
|
|
21
|
+
public string Tier => "advanced";
|
|
22
|
+
public bool Write => true;
|
|
23
|
+
public string Description => "Change the type of 1–200 elements using explicit element_id/type_id pairs. Revit validates each type against the target; invalid or pinned targets are reported per update. Some type changes replace the original element: always use resulting_id/unique_id afterward. Each update reports inherited_state: instance values, traits and, for the first 20 updates, views that hide or override the retyped element. Default partial success; atomic=true rolls back all if any update fails. preview=true commit-validates then rolls back, and replacement IDs from previews are temporary. Revit constraints can affect connected/hosted elements; results describe requested targets, not every dependent effect.";
|
|
24
|
+
public object ParametersSchema => new
|
|
25
|
+
{
|
|
26
|
+
type = "object", properties = new
|
|
27
|
+
{
|
|
28
|
+
updates = new { type = "array", minItems = 1, maxItems = 200, items = new
|
|
29
|
+
{
|
|
30
|
+
type = "object", properties = new { element_id = new { type = "integer", minimum = 1 }, type_id = new { type = "integer", minimum = 1 } },
|
|
31
|
+
required = new[] { "element_id", "type_id" },
|
|
32
|
+
} },
|
|
33
|
+
preview = ModelEditInputs.PreviewSchema,
|
|
34
|
+
atomic = new { type = "boolean", description = "Roll back the complete batch if any update fails. Default false." },
|
|
35
|
+
}, required = new[] { "updates" },
|
|
36
|
+
};
|
|
37
|
+
public object Execute(JsonElement args, ToolContext context)
|
|
38
|
+
{
|
|
39
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
40
|
+
if (!args.TryGetProperty("updates", out var updates) || updates.ValueKind != JsonValueKind.Array || updates.GetArrayLength() is < 1 or > 200)
|
|
41
|
+
throw new ArgumentException("updates must contain 1–200 element_id/type_id pairs.");
|
|
42
|
+
var seen = new HashSet<long>();
|
|
43
|
+
var steps = new List<ModelEditBatch.Step>();
|
|
44
|
+
foreach (var update in updates.EnumerateArray())
|
|
45
|
+
{
|
|
46
|
+
long elementId = JsonArgs.GetLong(update, "element_id") ?? 0;
|
|
47
|
+
long typeId = JsonArgs.GetLong(update, "type_id") ?? 0;
|
|
48
|
+
if (elementId <= 0 || typeId <= 0 || !seen.Add(elementId)) throw new ArgumentException("Each update needs positive IDs, and element_id must be unique within the batch.");
|
|
49
|
+
int index = steps.Count;
|
|
50
|
+
steps.Add(new(new() { ["element_id"] = elementId, ["type_id"] = typeId }, () =>
|
|
51
|
+
{
|
|
52
|
+
var element = doc.GetElement(new ElementId(elementId)) ?? throw new ArgumentException($"Element {elementId} not found.");
|
|
53
|
+
if (element.Pinned) throw new ArgumentException("Target is pinned; it was not unpinned.");
|
|
54
|
+
var type = doc.GetElement(new ElementId(typeId)) as ElementType ?? throw new ArgumentException($"Type {typeId} not found.");
|
|
55
|
+
if (!element.IsValidType(type.Id)) throw new ArgumentException($"Type {typeId} is not valid for element {elementId}.");
|
|
56
|
+
var before = ModelEditInputs.Snapshot(element);
|
|
57
|
+
var replacement = element.ChangeTypeId(type.Id);
|
|
58
|
+
doc.Regenerate();
|
|
59
|
+
var resultId = replacement == ElementId.InvalidElementId ? new ElementId(elementId) : replacement;
|
|
60
|
+
var result = doc.GetElement(resultId) ?? throw new InvalidOperationException("Changed element could not be resolved.");
|
|
61
|
+
return new() { ["before"] = before, ["after"] = ModelEditInputs.Snapshot(result),
|
|
62
|
+
["resulting_id"] = result.Id.Value, ["unique_id"] = result.UniqueId,
|
|
63
|
+
["replaced"] = result.Id.Value != elementId,
|
|
64
|
+
["resulting_id_is_temporary"] = result.Id.Value != elementId && JsonArgs.GetBool(args, "preview", false),
|
|
65
|
+
// What the retyped element keeps: instance values, traits and per-view graphics (inv:derived-state-reported).
|
|
66
|
+
["inherited_state"] = InheritedState.OfElement(result, result.Id.Value != elementId ? elementId : null, viewGraphics: index < ViewGraphicsLimit) };
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
var batch = ModelEditBatch.Run(doc, Name, args, steps);
|
|
70
|
+
foreach (var row in batch.Proposed)
|
|
71
|
+
if (row["replaced"] is true) row["resulting_id_is_temporary"] = true;
|
|
72
|
+
return batch.Payload;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
namespace RevitBridge.Tools
|
|
2
|
+
{
|
|
3
|
+
/// <summary>
|
|
4
|
+
/// Net element changes of one tool call, from Revit's DocumentChanged operations. Pure, so it
|
|
5
|
+
/// is tested offline. A rolled-back or undone group discards what was recorded before it, so a
|
|
6
|
+
/// preview (commit inside a rolled-back group) reports no net change.
|
|
7
|
+
/// </summary>
|
|
8
|
+
internal sealed class ChangeSet
|
|
9
|
+
{
|
|
10
|
+
private readonly HashSet<long> _added = new(), _modified = new(), _deleted = new();
|
|
11
|
+
public bool Observed { get; private set; }
|
|
12
|
+
public bool RolledBack { get; private set; }
|
|
13
|
+
public IReadOnlyCollection<long> Added => _added;
|
|
14
|
+
public IReadOnlyCollection<long> Modified => _modified;
|
|
15
|
+
public IReadOnlyCollection<long> Deleted => _deleted;
|
|
16
|
+
|
|
17
|
+
public void Record(string operation, IEnumerable<long> added, IEnumerable<long> modified, IEnumerable<long> deleted)
|
|
18
|
+
{
|
|
19
|
+
Observed = true;
|
|
20
|
+
if (operation is not ("TransactionCommitted" or "TransactionRedone"))
|
|
21
|
+
{
|
|
22
|
+
// Undo or rollback: everything recorded in this call is no longer a net change.
|
|
23
|
+
_added.Clear(); _modified.Clear(); _deleted.Clear();
|
|
24
|
+
RolledBack = true;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
foreach (var id in added) { _added.Add(id); _deleted.Remove(id); }
|
|
28
|
+
foreach (var id in modified) if (!_added.Contains(id)) _modified.Add(id);
|
|
29
|
+
foreach (var id in deleted)
|
|
30
|
+
{
|
|
31
|
+
_modified.Remove(id);
|
|
32
|
+
// Created and deleted within one call leaves nothing behind.
|
|
33
|
+
if (!_added.Remove(id)) _deleted.Add(id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public bool IsEmpty => _added.Count == 0 && _modified.Count == 0 && _deleted.Count == 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
using Autodesk.Revit.DB.Architecture;
|
|
4
|
+
using Autodesk.Revit.DB.Mechanical;
|
|
5
|
+
|
|
6
|
+
namespace RevitBridge.Tools;
|
|
7
|
+
|
|
8
|
+
internal sealed class CreateTags : ITool
|
|
9
|
+
{
|
|
10
|
+
public string Name => "create_tags";
|
|
11
|
+
public IReadOnlyList<string> DocumentKinds => DocumentKind.ProjectOnly;
|
|
12
|
+
public IReadOnlyList<string> Keywords => new[] { "tag", "label", "annotate", "annotation", "room tag", "door tag" };
|
|
13
|
+
public IReadOnlyList<ToolLimit> Limits => new[]
|
|
14
|
+
{
|
|
15
|
+
new ToolLimit("Tagging elements inside linked models", "api", "Reference.CreateLinkReference; IndependentTag.Create"),
|
|
16
|
+
new ToolLimit("Tagging faces or subelements", "api", "IndependentTag.Create with a face Reference"),
|
|
17
|
+
new ToolLimit("Creating or loading tag families", "api", "Document.LoadFamily"),
|
|
18
|
+
new ToolLimit("Text notes, dimensions and other annotation", "api", "TextNote.Create; Creation.ItemFactoryBase.NewDimension"),
|
|
19
|
+
};
|
|
20
|
+
public string? Verification => "capture";
|
|
21
|
+
public string Label => "Create Tags";
|
|
22
|
+
public string Tier => "advanced";
|
|
23
|
+
public bool Write => true;
|
|
24
|
+
public string Description => "Create up to 100 tags for host-document elements in one explicit view using a loaded tag FamilySymbol. kind=element uses IndependentTag; room, space and area use their corresponding spatial tag APIs. Targets contain element_id and head_position [x,y,z] in document internal coordinates with explicit length unit. head_position always means the tag head, including with leader=true. Spatial tags require a compatible plan view and positions at that spatial element's level; orientation applies only to element tags. Templates, perspective views and unlocked 3D views cannot host independent tags. Default partial success; atomic=true rolls back all on any failure. preview=true commit-validates then rolls back; all proposed tag IDs are temporary. Linked targets and face/subelement references are outside this tool.";
|
|
25
|
+
public object ParametersSchema => new
|
|
26
|
+
{
|
|
27
|
+
type = "object", properties = new
|
|
28
|
+
{
|
|
29
|
+
kind = new { type = "string", @enum = new[] { "element", "room", "space", "area" } },
|
|
30
|
+
view_id = new { type = "integer", minimum = 1 }, tag_type_id = new { type = "integer", minimum = 1 }, unit = ModelEditInputs.LengthUnitSchema,
|
|
31
|
+
targets = new { type = "array", minItems = 1, maxItems = 100, items = new { type = "object", properties = new { element_id = new { type = "integer", minimum = 1 }, head_position = ModelEditInputs.VectorSchema("Tag head in document internal coordinates, in unit.") }, required = new[] { "element_id", "head_position" } } },
|
|
32
|
+
leader = new { type = "boolean" }, orientation = new { type = "string", @enum = new[] { "horizontal", "vertical" } },
|
|
33
|
+
preview = ModelEditInputs.PreviewSchema, atomic = new { type = "boolean" },
|
|
34
|
+
}, required = new[] { "kind", "view_id", "tag_type_id", "unit", "targets" },
|
|
35
|
+
};
|
|
36
|
+
public object Execute(JsonElement args, ToolContext context)
|
|
37
|
+
{
|
|
38
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
39
|
+
string kind = JsonArgs.GetString(args, "kind") ?? "";
|
|
40
|
+
if (kind is not ("element" or "room" or "space" or "area")) throw new ArgumentException("Unknown tag kind.");
|
|
41
|
+
var view = doc.GetElement(new ElementId(JsonArgs.GetLong(args, "view_id") ?? 0)) as View ?? throw new ArgumentException("view_id is not a view.");
|
|
42
|
+
if (view.IsTemplate || (view is View3D three && (three.IsPerspective || !three.IsLocked))) throw new ArgumentException("Tags require a non-template view; 3D views must be orthographic and locked.");
|
|
43
|
+
if (kind != "element" && view is not ViewPlan) throw new ArgumentException("Spatial tags require a plan view.");
|
|
44
|
+
var type = doc.GetElement(new ElementId(JsonArgs.GetLong(args, "tag_type_id") ?? 0)) as FamilySymbol ?? throw new ArgumentException("tag_type_id must identify a loaded tag FamilySymbol.");
|
|
45
|
+
bool leader = false;
|
|
46
|
+
if (args.TryGetProperty("leader", out var leaderValue))
|
|
47
|
+
{
|
|
48
|
+
if (leaderValue.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) throw new ArgumentException("leader must be boolean.");
|
|
49
|
+
leader = leaderValue.GetBoolean();
|
|
50
|
+
}
|
|
51
|
+
if (kind != "element" && args.TryGetProperty("orientation", out _)) throw new ArgumentException("orientation applies only to element tags.");
|
|
52
|
+
var orientation = (JsonArgs.GetString(args, "orientation") ?? "horizontal") switch
|
|
53
|
+
{
|
|
54
|
+
"horizontal" => TagOrientation.Horizontal, "vertical" => TagOrientation.Vertical, _ => throw new ArgumentException("Invalid orientation."),
|
|
55
|
+
};
|
|
56
|
+
if (!args.TryGetProperty("targets", out var targets) || targets.ValueKind != JsonValueKind.Array || targets.GetArrayLength() is < 1 or > 100) throw new ArgumentException("targets must contain 1–100 tag requests.");
|
|
57
|
+
double scale = ModelEditInputs.LengthScale(args);
|
|
58
|
+
var steps = new List<ModelEditBatch.Step>();
|
|
59
|
+
foreach (var input in targets.EnumerateArray())
|
|
60
|
+
{
|
|
61
|
+
if (input.ValueKind != JsonValueKind.Object) throw new ArgumentException("Each target must be an object.");
|
|
62
|
+
long id = JsonArgs.GetLong(input, "element_id") ?? 0;
|
|
63
|
+
if (id <= 0) throw new ArgumentException("Target element_id must be positive.");
|
|
64
|
+
var point = ModelEditInputs.Vector(input, "head_position").Multiply(scale);
|
|
65
|
+
steps.Add(new(new() { ["element_id"] = id }, () =>
|
|
66
|
+
{
|
|
67
|
+
var element = doc.GetElement(new ElementId(id)) ?? throw new ArgumentException($"Element {id} not found.");
|
|
68
|
+
if (!type.IsActive) { type.Activate(); doc.Regenerate(); }
|
|
69
|
+
Element tag;
|
|
70
|
+
if (kind == "element")
|
|
71
|
+
{
|
|
72
|
+
var independent = IndependentTag.Create(doc, type.Id, view.Id, new Reference(element), false, orientation, point);
|
|
73
|
+
independent.HasLeader = leader;
|
|
74
|
+
independent.TagHeadPosition = point;
|
|
75
|
+
tag = independent;
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
{
|
|
79
|
+
if (element.Location is not LocationPoint location || view.GenLevel == null || element.LevelId != view.GenLevel.Id)
|
|
80
|
+
throw new ArgumentException("A spatial tag target must be placed on the plan view's level.");
|
|
81
|
+
var anchor = new UV(location.Point.X, location.Point.Y);
|
|
82
|
+
SpatialElementTag spatial = kind switch
|
|
83
|
+
{
|
|
84
|
+
"room" when element is Room => doc.Create.NewRoomTag(new LinkElementId(element.Id), anchor, view.Id),
|
|
85
|
+
"space" when element is Space space => doc.Create.NewSpaceTag(space, anchor, view),
|
|
86
|
+
"area" when element is Area area => doc.Create.NewAreaTag((ViewPlan)view, area, anchor),
|
|
87
|
+
_ => throw new ArgumentException($"Element {id} does not match kind {kind}."),
|
|
88
|
+
};
|
|
89
|
+
if (!spatial.IsValidType(type.Id)) throw new ArgumentException("Tag type does not match the spatial tag category.");
|
|
90
|
+
var replacement = spatial.ChangeTypeId(type.Id);
|
|
91
|
+
if (replacement != ElementId.InvalidElementId) spatial = (SpatialElementTag)doc.GetElement(replacement);
|
|
92
|
+
spatial.HasLeader = leader; spatial.TagHeadPosition = point;
|
|
93
|
+
tag = spatial;
|
|
94
|
+
}
|
|
95
|
+
doc.Regenerate();
|
|
96
|
+
var actual = tag is IndependentTag independentResult ? independentResult.TagHeadPosition : ((SpatialElementTag)tag).TagHeadPosition;
|
|
97
|
+
return new() { ["tag_id"] = tag.Id.Value, ["unique_id"] = tag.UniqueId, ["tag_type_id"] = tag.GetTypeId().Value, ["view_id"] = tag.OwnerViewId.Value,
|
|
98
|
+
["kind"] = kind, ["head_position"] = new[] { actual.X, actual.Y, actual.Z }, ["unit"] = "feet", ["leader"] = leader, ["id_is_temporary"] = false,
|
|
99
|
+
// A retyped spatial tag may be replaced by a new element (inv:derived-state-reported).
|
|
100
|
+
["inherited_state"] = InheritedState.OfElement(tag, null) };
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
var batch = ModelEditBatch.Run(doc, Name, args, steps);
|
|
104
|
+
foreach (var row in batch.Proposed) row["id_is_temporary"] = true;
|
|
105
|
+
return batch.Payload;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using Autodesk.Revit.DB;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools;
|
|
5
|
+
|
|
6
|
+
internal sealed class DeleteElements : ITool
|
|
7
|
+
{
|
|
8
|
+
public string Name => "delete_elements";
|
|
9
|
+
public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
|
|
10
|
+
public IReadOnlyList<string> Keywords => new[] { "remove", "erase", "cleanup", "delete view", "delete sheet" };
|
|
11
|
+
public IReadOnlyList<ToolLimit> Limits => new[]
|
|
12
|
+
{
|
|
13
|
+
new ToolLimit("Pinned elements (rejected, never unpinned automatically)", "user", "Confirm unpinning (Element.Pinned) first"),
|
|
14
|
+
new ToolLimit("Elements inside linked models", "user", "Edit the linked model itself"),
|
|
15
|
+
new ToolLimit("One-step purge of unused types", "tool", "get_element_types with include_instance_count, then delete_elements"),
|
|
16
|
+
};
|
|
17
|
+
public string? Verification => "reread";
|
|
18
|
+
public string Label => "Delete Elements";
|
|
19
|
+
public string Tier => "advanced";
|
|
20
|
+
public bool Write => true;
|
|
21
|
+
public string Description => "Delete 1–200 explicitly selected elements in one atomic step, returning the full set of IDs returned by Revit's deletion API, including dependents. Use preview=true to inspect that set with commit validation and confirmed rollback. Optional expected_deleted_ids rejects a changed deletion set before commit; pass the full preview set to bind a later deletion to that set. Pinned requested elements are rejected. At most 10,000 deleted IDs are permitted; a larger cascade rolls back. Returned IDs include dependencies removed by Document.Delete, not a complete audit of surviving elements modified by Revit.";
|
|
22
|
+
public object ParametersSchema => new
|
|
23
|
+
{
|
|
24
|
+
type = "object", properties = new
|
|
25
|
+
{
|
|
26
|
+
element_ids = ModelEditInputs.IdsSchema,
|
|
27
|
+
preview = ModelEditInputs.PreviewSchema,
|
|
28
|
+
expected_deleted_ids = new { type = "array", minItems = 1, maxItems = 10000, uniqueItems = true, items = new { type = "integer", minimum = 1 }, description = "Exact full deleted_ids set from a previous preview; mismatches roll back." },
|
|
29
|
+
}, required = new[] { "element_ids" },
|
|
30
|
+
};
|
|
31
|
+
public object Execute(JsonElement args, ToolContext context)
|
|
32
|
+
{
|
|
33
|
+
var doc = context.Document ?? throw new NoActiveDocumentException();
|
|
34
|
+
var ids = ModelEditInputs.Ids(args);
|
|
35
|
+
List<long>? expected = args.TryGetProperty("expected_deleted_ids", out _) ? JsonArgs.GetLongArray(args, "expected_deleted_ids") : null;
|
|
36
|
+
if (expected != null && (expected.Count is < 1 or > 10000 || expected.Any(x => x <= 0) || expected.Distinct().Count() != expected.Count))
|
|
37
|
+
throw new ArgumentException("expected_deleted_ids must contain 1–10,000 distinct positive IDs.");
|
|
38
|
+
return ModelEditBatch.Run(doc, Name, args, new[] { new ModelEditBatch.Step(
|
|
39
|
+
new() { ["element_ids"] = ids.Select(x => x.Value).ToArray() }, () =>
|
|
40
|
+
{
|
|
41
|
+
foreach (var id in ids)
|
|
42
|
+
{
|
|
43
|
+
var element = doc.GetElement(id) ?? throw new ArgumentException($"Element {id.Value} not found.");
|
|
44
|
+
if (element.Pinned) throw new ArgumentException($"Element {id.Value} is pinned; no elements were unpinned.");
|
|
45
|
+
}
|
|
46
|
+
var deleted = doc.Delete(ids).Select(id => id.Value).OrderBy(id => id).ToArray();
|
|
47
|
+
if (deleted.Length > 10000) throw new ArgumentException("Deletion exceeds the 10,000-ID cascade limit.");
|
|
48
|
+
if (expected != null && !deleted.ToHashSet().SetEquals(expected)) throw new ArgumentException("The deletion set changed since the preview; deletion was rolled back. Run a fresh preview.");
|
|
49
|
+
return new() { ["deleted_ids"] = deleted, ["deleted_count"] = deleted.Length,
|
|
50
|
+
["dependent_ids"] = deleted.Except(ids.Select(x => x.Value)).ToArray() };
|
|
51
|
+
}) }).Payload;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -28,10 +28,20 @@ internal static class DocumentGuard
|
|
|
28
28
|
public static bool AlwaysRequiresIdentity(string toolName)
|
|
29
29
|
=> toolName is "set_parameters" or "execute_csharp" or "export_documents" or "open_view";
|
|
30
30
|
|
|
31
|
+
/// <summary>Project or family: the kind of the active document, as tools declare it.</summary>
|
|
32
|
+
public static string KindOf(Document document) => document.IsFamilyDocument ? DocumentKind.Family : DocumentKind.Project;
|
|
33
|
+
|
|
34
|
+
/// <summary>Refuse a tool in a document kind it does not declare, before it runs (inv:document-kind-declared).</summary>
|
|
35
|
+
public static void CheckKind(ITool tool, Document document)
|
|
36
|
+
{
|
|
37
|
+
if (DocumentKind.Refusal(tool.Name, tool.DocumentKinds, KindOf(document), document.Title) is { } refusal)
|
|
38
|
+
throw new ArgumentException(refusal);
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
/// <summary>Must run on the Revit API thread immediately before the tool action.</summary>
|
|
32
|
-
public static void CheckForTool(JsonElement args, Document document, string toolName)
|
|
42
|
+
public static void CheckForTool(JsonElement args, Document document, string toolName, bool writes = false)
|
|
33
43
|
{
|
|
34
|
-
bool required = AlwaysRequiresIdentity(toolName)
|
|
44
|
+
bool required = writes || AlwaysRequiresIdentity(toolName)
|
|
35
45
|
|| (toolName == "manage_selection"
|
|
36
46
|
&& (!string.Equals((JsonArgs.GetString(args, "action") ?? "get").Trim(), "get", StringComparison.OrdinalIgnoreCase)
|
|
37
47
|
|| JsonArgs.GetBool(args, "isolate_in_view", false)));
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
using Autodesk.Revit.DB;
|
|
2
|
+
|
|
3
|
+
namespace RevitBridge.Tools
|
|
4
|
+
{
|
|
5
|
+
/// <summary>
|
|
6
|
+
/// The one place a tool assigns a name or sheet number (inv:existing-objects-not-reused).
|
|
7
|
+
/// A name another object of the same kind already uses is rejected before Revit is asked,
|
|
8
|
+
/// with the existing object's identity in the failure, so the caller learns that the object
|
|
9
|
+
/// predates this call instead of silently reusing or editing it. There is deliberately no
|
|
10
|
+
/// "reuse" option: reusing someone else's object needs the user's decision, after which the
|
|
11
|
+
/// caller works with that object's ID directly. The gate in check-tool-documentation.mjs
|
|
12
|
+
/// rejects any other Name or SheetNumber assignment in a tool.
|
|
13
|
+
/// </summary>
|
|
14
|
+
internal static class ElementNames
|
|
15
|
+
{
|
|
16
|
+
public const string CollisionKey = "name_collision";
|
|
17
|
+
|
|
18
|
+
/// <summary>Rename element, or fail with the colliding object's identity.</summary>
|
|
19
|
+
public static void Assign(Element element, string name)
|
|
20
|
+
{
|
|
21
|
+
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name must be nonempty.");
|
|
22
|
+
if (element.Name == name) return;
|
|
23
|
+
if (RequiresUniqueName(element) && FindSameKind(element, name) is { } existing) throw Collision(element, existing, "name", name);
|
|
24
|
+
element.Name = name;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// <summary>
|
|
28
|
+
/// Kinds whose names Revit keeps unique. Sheets (identified by number), rooms and other
|
|
29
|
+
/// objects may legitimately share a name, so they are not checked here.
|
|
30
|
+
/// </summary>
|
|
31
|
+
public static bool RequiresUniqueName(Element element) =>
|
|
32
|
+
element is (View and not ViewSheet) or Level or Grid or ElementType or Material or ParameterFilterElement;
|
|
33
|
+
|
|
34
|
+
/// <summary>Set a sheet number, or fail with the sheet that already has it.</summary>
|
|
35
|
+
public static void AssignSheetNumber(ViewSheet sheet, string number)
|
|
36
|
+
{
|
|
37
|
+
if (string.IsNullOrWhiteSpace(number)) throw new ArgumentException("number must be nonempty.");
|
|
38
|
+
if (sheet.SheetNumber == number) return;
|
|
39
|
+
var existing = new FilteredElementCollector(sheet.Document).OfClass(typeof(ViewSheet)).Cast<ViewSheet>()
|
|
40
|
+
.FirstOrDefault(other => other.Id != sheet.Id && other.SheetNumber == number);
|
|
41
|
+
if (existing != null) throw Collision(sheet, existing, "sheet number", number);
|
|
42
|
+
sheet.SheetNumber = number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// <summary>Create a family type in a family document, or fail when the name is taken.</summary>
|
|
46
|
+
public static FamilyType NewFamilyType(FamilyManager manager, string name)
|
|
47
|
+
{
|
|
48
|
+
EnsureFamilyTypeFree(manager, name);
|
|
49
|
+
return manager.NewType(name);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// <summary>Rename the current family type, or fail when another type has the name.</summary>
|
|
53
|
+
public static void RenameCurrentFamilyType(FamilyManager manager, string name)
|
|
54
|
+
{
|
|
55
|
+
if (manager.CurrentType?.Name == name) return;
|
|
56
|
+
EnsureFamilyTypeFree(manager, name);
|
|
57
|
+
manager.RenameCurrentType(name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// <summary>Family types are not elements and have no ID; the collision names the type instead.</summary>
|
|
61
|
+
private static void EnsureFamilyTypeFree(FamilyManager manager, string name)
|
|
62
|
+
{
|
|
63
|
+
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name must be nonempty.");
|
|
64
|
+
if (!manager.Types.Cast<FamilyType>().Any(type => type.Name == name)) return;
|
|
65
|
+
var error = new ArgumentException(Message("family type", "name", name, null));
|
|
66
|
+
error.Data[CollisionKey] = new Dictionary<string, object?> { ["existing_id"] = null, ["kind"] = "family type", ["name"] = name };
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// <summary>
|
|
71
|
+
/// An object of the same kind with this exact name: same class, and for views the same
|
|
72
|
+
/// view type (a floor plan and a ceiling plan may share a name). Null when the name is free.
|
|
73
|
+
/// </summary>
|
|
74
|
+
public static Element? FindSameKind(Element element, string name)
|
|
75
|
+
{
|
|
76
|
+
FilteredElementCollector collector;
|
|
77
|
+
// Some API classes (for example Room) are not native filter classes; Revit's own
|
|
78
|
+
// uniqueness check still applies to them when the name is assigned.
|
|
79
|
+
try { collector = new FilteredElementCollector(element.Document).OfClass(element.GetType()); }
|
|
80
|
+
catch (Autodesk.Revit.Exceptions.ArgumentException) { return null; }
|
|
81
|
+
return collector.FirstOrDefault(other => other.Id != element.Id && other.Name == name
|
|
82
|
+
&& (element is not View view || (other is View otherView && otherView.ViewType == view.ViewType && otherView.IsTemplate == view.IsTemplate))
|
|
83
|
+
&& (element is not ElementType || other.Category?.Id == element.Category?.Id));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// <summary>Pure message text, shared by every tool and tested offline.</summary>
|
|
87
|
+
public static string Message(string kind, string what, string value, long? existingId) =>
|
|
88
|
+
$"A {kind} with the {what} '{value}' already exists{(existingId is long id ? $" (id {id})" : "")}. Nothing was given that {what}. "
|
|
89
|
+
+ "That object existed before this call: do not edit, reuse, replace or delete it unless the user asks. "
|
|
90
|
+
+ "Ask the user, or choose a distinct value and report the collision.";
|
|
91
|
+
|
|
92
|
+
private static ArgumentException Collision(Element target, Element existing, string what, string value)
|
|
93
|
+
{
|
|
94
|
+
string kind = target is View view ? $"{view.ViewType} view" : target.GetType().Name;
|
|
95
|
+
var error = new ArgumentException(Message(kind, what, value, existing.Id.Value));
|
|
96
|
+
error.Data[CollisionKey] = new Dictionary<string, object?>
|
|
97
|
+
{
|
|
98
|
+
["existing_id"] = existing.Id.Value, ["existing_unique_id"] = existing.UniqueId, ["kind"] = kind, [what.Replace(' ', '_')] = value,
|
|
99
|
+
};
|
|
100
|
+
return error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using System.Text.Json.Nodes;
|
|
3
|
+
|
|
4
|
+
namespace RevitBridge.Tools;
|
|
5
|
+
|
|
6
|
+
internal static class ElementQueryScope
|
|
7
|
+
{
|
|
8
|
+
public static JsonObject Schema()
|
|
9
|
+
{
|
|
10
|
+
var schema = JsonSerializer.SerializeToNode(new GetElements().ParametersSchema)!.AsObject();
|
|
11
|
+
foreach (string key in new[] { "count_only", "fields", "offset", "limit", "parameter_names", "include_type_parameters" })
|
|
12
|
+
schema["properties"]!.AsObject().Remove(key);
|
|
13
|
+
schema["description"] = "Filter the whole matching scope. Query paging and projections are not supported here.";
|
|
14
|
+
schema["additionalProperties"] = false;
|
|
15
|
+
return schema;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public static JsonObject Parse(JsonElement args)
|
|
19
|
+
{
|
|
20
|
+
var query = args.TryGetProperty("query", out var input) ? JsonNode.Parse(input.GetRawText()) as JsonObject : new JsonObject();
|
|
21
|
+
if (query == null) throw new ArgumentException("query must be an object.");
|
|
22
|
+
var allowed = Schema()["properties"]!.AsObject();
|
|
23
|
+
foreach (var property in query)
|
|
24
|
+
if (!allowed.ContainsKey(property.Key)) throw new ArgumentException($"query does not support '{property.Key}'. It always uses the whole matching scope.");
|
|
25
|
+
return query;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
using Autodesk.Revit.DB;
|
|
2
|
+
|
|
3
|
+
namespace RevitBridge.Tools
|
|
4
|
+
{
|
|
5
|
+
/// <summary>
|
|
6
|
+
/// Shared classification of special or system-owned objects (inv:special-objects-flagged).
|
|
7
|
+
/// Tools never mix these silently into ordinary results: they either flag them here or
|
|
8
|
+
/// exclude them and count the exclusion. Only non-default traits are emitted, so
|
|
9
|
+
/// ordinary elements add nothing to a result. New tools reuse this instead of inventing
|
|
10
|
+
/// their own checks, and new trait kinds are added here once.
|
|
11
|
+
/// </summary>
|
|
12
|
+
internal static class ElementTraits
|
|
13
|
+
{
|
|
14
|
+
/// <summary>Non-default traits of an element, or null when it has none.</summary>
|
|
15
|
+
public static Dictionary<string, object>? For(Element element)
|
|
16
|
+
{
|
|
17
|
+
var traits = new Dictionary<string, object>();
|
|
18
|
+
if (element is ScheduleSheetInstance { IsTitleblockRevisionSchedule: true })
|
|
19
|
+
traits["titleblock_revision_schedule"] = true;
|
|
20
|
+
if (element is ViewSchedule { IsTitleblockRevisionSchedule: true })
|
|
21
|
+
traits["titleblock_revision_schedule"] = true;
|
|
22
|
+
if (element is ViewSheet { IsPlaceholder: true })
|
|
23
|
+
traits["placeholder_sheet"] = true;
|
|
24
|
+
if (element is View view)
|
|
25
|
+
{
|
|
26
|
+
if (view.IsTemplate)
|
|
27
|
+
traits["view_template"] = true;
|
|
28
|
+
var primary = view.GetPrimaryViewId();
|
|
29
|
+
if (primary != null && primary != ElementId.InvalidElementId)
|
|
30
|
+
traits["dependent_view_of"] = primary.Value;
|
|
31
|
+
}
|
|
32
|
+
if (element.GroupId is { } group && group != ElementId.InvalidElementId)
|
|
33
|
+
traits["group_id"] = group.Value;
|
|
34
|
+
if (element.DesignOption is { } option)
|
|
35
|
+
traits["design_option_id"] = option.Id.Value;
|
|
36
|
+
if (element.Pinned)
|
|
37
|
+
traits["pinned"] = true;
|
|
38
|
+
return traits.Count > 0 ? traits : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// <summary>
|
|
42
|
+
/// Kind of a sheet placement. A titleblock revision schedule is part of the titleblock,
|
|
43
|
+
/// not content someone placed, so it has its own kind and cannot be moved.
|
|
44
|
+
/// </summary>
|
|
45
|
+
public static string PlacementKind(Element placement) => placement switch
|
|
46
|
+
{
|
|
47
|
+
Viewport => "viewport",
|
|
48
|
+
ScheduleSheetInstance { IsTitleblockRevisionSchedule: true } => "titleblock_revision_schedule",
|
|
49
|
+
ScheduleSheetInstance => "schedule",
|
|
50
|
+
_ => "other",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|