/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * IDS (Information Delivery Specification) state slice * * Manages IDS validation state, results, and viewer integration. */ import type { StateCreator } from 'zustand'; import type { IDSAuditReport, IDSDocument, IDSValidationReport, IDSSpecificationResult, IDSEntityResult, SupportedLocale, ValidationProgress, } from '@ifc-lite/ids'; // ============================================================================ // Types // ============================================================================ /** Display options for IDS visualization */ export interface IDSDisplayOptions { /** Highlight failed entities in 3D view */ highlightFailed: boolean; /** Highlight passed entities in 3D view */ highlightPassed: boolean; /** Color for failed entities [R, G, B, A] */ failedColor: [number, number, number, number]; /** Color for passed entities [R, G, B, A] */ passedColor: [number, number, number, number]; } /** IDS filter mode */ export type IDSFilterMode = 'all' | 'failed' | 'passed'; /** * Scope for the isolate/color controls. * - 'ids': act on the whole validation report (every specification combined) * - 'spec': act on the currently active specification only */ export type IDSIsolationScope = 'ids' | 'spec'; /** * Which IDS isolate action is currently applied, so the panel can show the * active button as pressed and toggle it off on a second click. `null` when * IDS is not isolating. */ export type IDSIsolateMode = 'failed' | 'passed' | 'involved' | null; export interface IDSSliceState { /** Loaded IDS document */ idsDocument: IDSDocument | null; /** * Audit report for the loaded IDS document itself — flags authoring * issues (missing attributes, invalid IFC entity references, regex * errors, etc.). Distinct from `idsValidationReport`, which describes * how an IFC model conforms to the IDS. */ idsAuditReport: IDSAuditReport | null; /** Whether the audit pipeline is currently running. */ idsAuditing: boolean; /** Validation report after running validation */ idsValidationReport: IDSValidationReport | null; /** Currently active specification (for filtering results) */ idsActiveSpecificationId: string | null; /** Currently selected entity in results */ idsActiveEntityId: { modelId: string; expressId: number } | null; /** IDS panel visibility */ idsPanelVisible: boolean; /** Loading state */ idsLoading: boolean; /** Validation progress */ idsProgress: ValidationProgress | null; /** Error message */ idsError: string | null; /** Current locale for translations */ idsLocale: SupportedLocale; /** Display options */ idsDisplayOptions: IDSDisplayOptions; /** Filter mode (show all, failed only, passed only) */ idsFilterMode: IDSFilterMode; /** * Whether the isolate/color controls act on the whole report ('ids', * default) or on the active specification only ('spec'). In 'spec' mode, * selecting a specification isolates its involved elements (passed green, * failed red) so they can be reviewed in context — per issue #1236. */ idsIsolationScope: IDSIsolationScope; /** Which isolate action is currently applied (drives toggle + active state) */ idsIsolateMode: IDSIsolateMode; /** Cached set of failed entity IDs for efficient lookup */ idsFailedEntityIds: Set; // "modelId:expressId" format /** Cached set of passed entity IDs */ idsPassedEntityIds: Set; } export interface IDSSlice extends IDSSliceState { // Document actions setIdsDocument: (document: IDSDocument | null) => void; clearIdsDocument: () => void; // Audit actions setIdsAuditReport: (report: IDSAuditReport | null) => void; setIdsAuditing: (auditing: boolean) => void; // Validation actions setIdsValidationReport: (report: IDSValidationReport | null) => void; clearIdsValidationReport: () => void; setIdsProgress: (progress: ValidationProgress | null) => void; // Selection actions setIdsActiveSpecification: (specId: string | null) => void; setIdsActiveEntity: (ref: { modelId: string; expressId: number } | null) => void; // UI actions setIdsPanelVisible: (visible: boolean) => void; toggleIdsPanel: () => void; setIdsLoading: (loading: boolean) => void; setIdsError: (error: string | null) => void; setIdsLocale: (locale: SupportedLocale) => void; setIdsDisplayOptions: (options: Partial) => void; setIdsFilterMode: (mode: IDSFilterMode) => void; setIdsIsolationScope: (scope: IDSIsolationScope) => void; setIdsIsolateMode: (mode: IDSIsolateMode) => void; // Utility getters getActiveSpecificationResult: () => IDSSpecificationResult | null; getFailedEntitiesForSpec: (specId: string) => IDSEntityResult[]; getPassedEntitiesForSpec: (specId: string) => IDSEntityResult[]; getEntityResultById: (modelId: string, expressId: number) => IDSEntityResult | null; isEntityFailed: (modelId: string, expressId: number) => boolean; isEntityPassed: (modelId: string, expressId: number) => boolean; } // ============================================================================ // Default Values // ============================================================================ const DEFAULT_DISPLAY_OPTIONS: IDSDisplayOptions = { highlightFailed: true, highlightPassed: false, failedColor: [0.9, 0.2, 0.2, 1.0], // Red passedColor: [0.2, 0.8, 0.2, 1.0], // Green }; const getDefaultLocale = (): SupportedLocale => { // Try to get from browser language if (typeof navigator !== 'undefined') { const lang = navigator.language.split('-')[0]; if (lang === 'de' || lang === 'fr') { return lang as SupportedLocale; } } return 'en'; }; // ============================================================================ // Helper Functions // ============================================================================ /** * Build cached entity ID sets from validation report */ function buildEntityIdSets( report: IDSValidationReport | null ): { failed: Set; passed: Set } { const failed = new Set(); const passed = new Set(); if (!report) { return { failed, passed }; } for (const specResult of report.specificationResults) { for (const entityResult of specResult.entityResults) { const key = `${entityResult.modelId}:${entityResult.expressId}`; if (entityResult.passed) { passed.add(key); } else { failed.add(key); } } } return { failed, passed }; } // ============================================================================ // Slice Creator // ============================================================================ export const createIdsSlice: StateCreator = (set, get) => ({ // Initial state idsDocument: null, idsAuditReport: null, idsAuditing: false, idsValidationReport: null, idsActiveSpecificationId: null, idsActiveEntityId: null, idsPanelVisible: false, idsLoading: false, idsProgress: null, idsError: null, idsLocale: getDefaultLocale(), idsDisplayOptions: DEFAULT_DISPLAY_OPTIONS, idsFilterMode: 'all', idsIsolationScope: 'ids', idsIsolateMode: null, idsFailedEntityIds: new Set(), idsPassedEntityIds: new Set(), // Document actions setIdsDocument: (idsDocument) => set({ idsDocument, // Loading a new document invalidates any previous audit/validation // results — they were tied to a specific document instance. idsAuditReport: null, idsValidationReport: null, idsActiveSpecificationId: null, idsActiveEntityId: null, idsError: null, idsFailedEntityIds: new Set(), idsPassedEntityIds: new Set(), }), clearIdsDocument: () => set({ idsDocument: null, idsAuditReport: null, idsValidationReport: null, idsActiveSpecificationId: null, idsActiveEntityId: null, idsError: null, idsFailedEntityIds: new Set(), idsPassedEntityIds: new Set(), }), // Audit actions setIdsAuditReport: (idsAuditReport) => set({ idsAuditReport }), setIdsAuditing: (idsAuditing) => set({ idsAuditing }), // Validation actions setIdsValidationReport: (report) => { const { failed, passed } = buildEntityIdSets(report); set({ idsValidationReport: report, idsFailedEntityIds: failed, idsPassedEntityIds: passed, idsIsolateMode: null, idsError: null, idsProgress: null, }); }, clearIdsValidationReport: () => set({ idsValidationReport: null, idsActiveSpecificationId: null, idsActiveEntityId: null, idsIsolationScope: 'ids', idsIsolateMode: null, idsFailedEntityIds: new Set(), idsPassedEntityIds: new Set(), }), setIdsProgress: (idsProgress) => set({ idsProgress }), // Selection actions setIdsActiveSpecification: (idsActiveSpecificationId) => set({ idsActiveSpecificationId, idsActiveEntityId: null, }), setIdsActiveEntity: (idsActiveEntityId) => set({ idsActiveEntityId }), // UI actions setIdsPanelVisible: (idsPanelVisible) => set({ idsPanelVisible }), toggleIdsPanel: () => set((state) => ({ idsPanelVisible: !state.idsPanelVisible })), setIdsLoading: (idsLoading) => set({ idsLoading }), // Setting an error ends the run; but CLEARING the error (idsError = // null, e.g. at the start of a validation run) must NOT flip loading // off — doing so kept the progress UI, which is gated on `loading`, // hidden for the entire run even though progress was streaming in. setIdsError: (idsError) => set(idsError !== null ? { idsError, idsLoading: false } : { idsError }), setIdsLocale: (idsLocale) => set({ idsLocale }), setIdsDisplayOptions: (options) => set((state) => ({ idsDisplayOptions: { ...state.idsDisplayOptions, ...options }, })), setIdsFilterMode: (idsFilterMode) => set({ idsFilterMode }), setIdsIsolationScope: (idsIsolationScope) => set({ idsIsolationScope }), setIdsIsolateMode: (idsIsolateMode) => set({ idsIsolateMode }), // Utility getters getActiveSpecificationResult: () => { const state = get(); if (!state.idsValidationReport || !state.idsActiveSpecificationId) { return null; } return ( state.idsValidationReport.specificationResults.find( (r) => r.specification.id === state.idsActiveSpecificationId ) || null ); }, getFailedEntitiesForSpec: (specId) => { const state = get(); if (!state.idsValidationReport) return []; const specResult = state.idsValidationReport.specificationResults.find( (r) => r.specification.id === specId ); if (!specResult) return []; return specResult.entityResults.filter((e) => !e.passed); }, getPassedEntitiesForSpec: (specId) => { const state = get(); if (!state.idsValidationReport) return []; const specResult = state.idsValidationReport.specificationResults.find( (r) => r.specification.id === specId ); if (!specResult) return []; return specResult.entityResults.filter((e) => e.passed); }, getEntityResultById: (modelId, expressId) => { const state = get(); if (!state.idsValidationReport) return null; for (const specResult of state.idsValidationReport.specificationResults) { for (const entityResult of specResult.entityResults) { if ( entityResult.modelId === modelId && entityResult.expressId === expressId ) { return entityResult; } } } return null; }, isEntityFailed: (modelId, expressId) => { const state = get(); return state.idsFailedEntityIds.has(`${modelId}:${expressId}`); }, isEntityPassed: (modelId, expressId) => { const state = get(); return state.idsPassedEntityIds.has(`${modelId}:${expressId}`); }, });