Add per-project socket limit and Revit socket device-type flag

- Project setting maxSocketsPerCircuit (migration 0018,
  project.update-settings schema 4, snapshot schema 11); older command
  and snapshot versions keep or upgrade the value to null.
- Revit configuration schema 2 marks device types as socket; version 1
  stays readable and derives the flag from socketCount family rules.
- Settings dialog field and device-type card on the Revit page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Julian Appel 2026-10-30 21:18:08 +01:00
parent 85cbdbf8e0
commit e48042bde3
35 changed files with 3781 additions and 41 deletions

View file

@ -5,7 +5,12 @@ import {
type ExternalXlsxTransportConfiguration,
} from "../xlsx/external-xlsx-contracts.js";
export const externalModelConfigurationSchemaVersion = 1 as const;
// Version 2 adds `deviceTypes[].isSocket`. Version 1 stays readable wherever
// it is stored (configuration row, import-batch copies, history commands,
// snapshots); socket classification then derives from the family rules
// (`resolveSocketDeviceTypeIds`).
export const externalModelConfigurationSchemaVersion = 2 as const;
export const legacyExternalModelConfigurationSchemaVersion = 1 as const;
export const externalQuantityRules = ["one", "socketCount"] as const;
export type ExternalQuantityRule = (typeof externalQuantityRules)[number];
@ -21,6 +26,8 @@ export interface ExternalDeviceTypeConfiguration {
name: string;
category: CircuitGroupCategory;
defaults: ExternalDeviceTypeDefaults;
// Required from schema version 2, absent in version 1.
isSocket?: boolean;
}
export interface ExternalFamilyTypeRule {
@ -32,7 +39,9 @@ export interface ExternalFamilyTypeRule {
export interface ExternalModelConfiguration
extends ExternalXlsxTransportConfiguration {
schemaVersion: typeof externalModelConfigurationSchemaVersion;
schemaVersion:
| typeof externalModelConfigurationSchemaVersion
| typeof legacyExternalModelConfigurationSchemaVersion;
deviceTypes: ExternalDeviceTypeConfiguration[];
familyTypeRules: ExternalFamilyTypeRule[];
}
@ -49,7 +58,11 @@ export function createDefaultExternalModelConfiguration(): ExternalModelConfigur
export function assertExternalModelConfiguration(
value: unknown
): asserts value is ExternalModelConfiguration {
if (!isRecord(value) || value.schemaVersion !== externalModelConfigurationSchemaVersion) {
if (
!isRecord(value) ||
(value.schemaVersion !== externalModelConfigurationSchemaVersion &&
value.schemaVersion !== legacyExternalModelConfigurationSchemaVersion)
) {
throw new Error("Nicht unterstützte Revit-XLSX-Konfiguration.");
}
assertExactKeys(value, [
@ -72,7 +85,11 @@ export function assertExternalModelConfiguration(
const deviceTypeIds = new Set<string>();
const deviceTypeNames = new Set<string>();
for (const [index, deviceType] of value.deviceTypes.entries()) {
assertDeviceType(deviceType, index);
assertDeviceType(
deviceType,
index,
value.schemaVersion === externalModelConfigurationSchemaVersion
);
assertUnique(deviceTypeIds, deviceType.id, "Geräteart-ID");
assertUnique(deviceTypeNames, deviceType.name, "Geräteartname");
}
@ -153,9 +170,22 @@ function assertColumns(
}
}
function assertDeviceType(value: unknown, index: number): asserts value is ExternalDeviceTypeConfiguration {
function assertDeviceType(
value: unknown,
index: number,
hasSocketFlag: boolean
): asserts value is ExternalDeviceTypeConfiguration {
if (!isRecord(value)) throw new Error(`Geräteart ${index + 1} ist ungültig.`);
assertExactKeys(value, ["id", "name", "category", "defaults"], `Geräteart ${index + 1}`);
assertExactKeys(
value,
hasSocketFlag
? ["id", "name", "category", "defaults", "isSocket"]
: ["id", "name", "category", "defaults"],
`Geräteart ${index + 1}`
);
if (hasSocketFlag && typeof value.isSocket !== "boolean") {
throw new Error(`Geräteart ${index + 1}.isSocket muss ja oder nein sein.`);
}
assertUuid(value.id, `Geräteart ${index + 1}.id`);
assertNonEmptyString(value.name, `Geräteart ${index + 1}.name`);
if (!(["lighting", "single_phase", "three_phase"] as unknown[]).includes(value.category)) {