From f57870888efca146a5e4c10b73de85189c0b3b9c Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Wed, 21 Oct 2026 14:56:23 +0200 Subject: [PATCH 1/6] fix(meshEdges): Hide edge attribute where OpenGeode has no edges viewable --- .../Viewer/Generic/Mesh/EdgesOptions.vue | 14 +++++++++++++- app/components/Viewer/Grid/2D/EdgesOptions.vue | 6 +++++- app/components/Viewer/Grid/3D/EdgesOptions.vue | 6 +++++- app/components/Viewer/Solid/EdgesOptions.vue | 6 +++++- app/components/Viewer/Surface/EdgesOptions.vue | 6 +++++- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/app/components/Viewer/Generic/Mesh/EdgesOptions.vue b/app/components/Viewer/Generic/Mesh/EdgesOptions.vue index 284da8714..dd10021dc 100644 --- a/app/components/Viewer/Generic/Mesh/EdgesOptions.vue +++ b/app/components/Viewer/Generic/Mesh/EdgesOptions.vue @@ -18,13 +18,24 @@ const dataStyleStore = useDataStyleStore(); const hybridViewerStore = useHybridViewerStore(); const { applyBatchStyle } = useBatchStyle(); +interface CapabilityConfig { + available?: boolean; + hasColorMap?: boolean; +} + interface Props { itemProps: ItemProps & { index?: number }; btnImage: string; tooltip?: string; + capabilities?: Record; } -const { itemProps, btnImage, tooltip = "Edges options" } = defineProps(); +const { + itemProps, + btnImage, + tooltip = "Edges options", + capabilities = {}, +} = defineProps(); const id = toRef(() => itemProps.id); @@ -251,6 +262,7 @@ async function setEdgeAttributeRangesPerData(ranges: RangesPerData): Promise diff --git a/app/components/Viewer/Grid/2D/EdgesOptions.vue b/app/components/Viewer/Grid/2D/EdgesOptions.vue index 9045ac83a..a93852cde 100644 --- a/app/components/Viewer/Grid/2D/EdgesOptions.vue +++ b/app/components/Viewer/Grid/2D/EdgesOptions.vue @@ -11,5 +11,9 @@ const { itemProps } = defineProps(); diff --git a/app/components/Viewer/Grid/3D/EdgesOptions.vue b/app/components/Viewer/Grid/3D/EdgesOptions.vue index 59eb498fb..9ba592e70 100644 --- a/app/components/Viewer/Grid/3D/EdgesOptions.vue +++ b/app/components/Viewer/Grid/3D/EdgesOptions.vue @@ -11,5 +11,9 @@ const { itemProps } = defineProps(); diff --git a/app/components/Viewer/Solid/EdgesOptions.vue b/app/components/Viewer/Solid/EdgesOptions.vue index 9309f30c5..49880691a 100644 --- a/app/components/Viewer/Solid/EdgesOptions.vue +++ b/app/components/Viewer/Solid/EdgesOptions.vue @@ -11,5 +11,9 @@ const { itemProps } = defineProps(); diff --git a/app/components/Viewer/Surface/EdgesOptions.vue b/app/components/Viewer/Surface/EdgesOptions.vue index b95655aa1..4101603aa 100644 --- a/app/components/Viewer/Surface/EdgesOptions.vue +++ b/app/components/Viewer/Surface/EdgesOptions.vue @@ -11,5 +11,9 @@ const { itemProps } = defineProps(); From f0c9ddfcd683c97defb69b3ca18d63618b07470c Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Wed, 21 Oct 2026 14:59:03 +0200 Subject: [PATCH 2/6] Extract shared component style helpers --- .../data_style/model/component_style.ts | 27 ++++++++++++++++++ .../data_style/model/points/component.ts | 28 ++++--------------- 2 files changed, 32 insertions(+), 23 deletions(-) create mode 100644 internal/stores/data_style/model/component_style.ts diff --git a/internal/stores/data_style/model/component_style.ts b/internal/stores/data_style/model/component_style.ts new file mode 100644 index 000000000..bfd11f94c --- /dev/null +++ b/internal/stores/data_style/model/component_style.ts @@ -0,0 +1,27 @@ +import type { RGBAColor } from "@ogw_front/utils/default_styles/constants"; + +function groupBy(componentIds: string[], key: (componentId: string) => string): string[][] { + const groups: Record = {}; + for (const componentId of componentIds) { + const groupKey = key(componentId); + groups[groupKey] ??= []; + groups[groupKey].push(componentId); + } + return Object.values(groups); +} + +interface AttributeStoredConfig { + minimum: number | undefined; + maximum: number | undefined; + colorMap: string | undefined; + no_data_color: RGBAColor; +} + +interface AttributeState { + name?: string; + item?: number; + storedConfigs?: Record>; +} + +export { groupBy }; +export type { AttributeState, AttributeStoredConfig }; diff --git a/internal/stores/data_style/model/points/component.ts b/internal/stores/data_style/model/points/component.ts index 629baae23..ffee244e0 100644 --- a/internal/stores/data_style/model/points/component.ts +++ b/internal/stores/data_style/model/points/component.ts @@ -1,4 +1,9 @@ // oxlint-disable eslint/max-lines +import { + type AttributeState, + type AttributeStoredConfig, + groupBy, +} from "@ogw_internal/stores/data_style/model/component_style"; import { DEFAULT_NO_DATA_COLOR, POINTS_DEFAULT_COLOR, @@ -34,29 +39,6 @@ type PointsRequestSchema = | PointsSchemas["color"] | PointsSchemas["attribute"]["vertex"]["attribute"]; -function groupBy(componentIds: string[], key: (componentId: string) => string): string[][] { - const groups: Record = {}; - for (const componentId of componentIds) { - const groupKey = key(componentId); - groups[groupKey] ??= []; - groups[groupKey].push(componentId); - } - return Object.values(groups); -} - -interface AttributeStoredConfig { - minimum: number | undefined; - maximum: number | undefined; - colorMap: string | undefined; - no_data_color: RGBAColor; -} - -interface AttributeState { - name?: string; - item?: number; - storedConfigs?: Record>; -} - interface PointsStyle { visibility?: boolean; size?: number; From f5e93fe64017c43c31ae3ddbc46504d12f52336b Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Wed, 21 Oct 2026 14:59:59 +0200 Subject: [PATCH 3/6] Surfaces and blocks edges get their own style per component type and per component: visibility, width, color with opacity, vertex attribute --- .../stores/data_style/model/blocks/edges.ts | 51 +++ .../stores/data_style/model/blocks/index.ts | 4 + .../data_style/model/edges/component.ts | 406 ++++++++++++++++++ .../stores/data_style/model/edges/index.ts | 6 +- .../data_style/model/edges/visibility.ts | 27 +- .../stores/data_style/model/edges/width.ts | 56 +++ internal/stores/data_style/model/index.ts | 4 +- .../stores/data_style/model/surfaces/edges.ts | 51 +++ .../stores/data_style/model/surfaces/index.ts | 4 + .../model/blocks_edges.nuxt.test.ts | 168 ++++++++ .../data_style/model/edges.nuxt.test.ts | 28 ++ .../model/surfaces_edges.nuxt.test.ts | 177 ++++++++ 12 files changed, 977 insertions(+), 5 deletions(-) create mode 100644 internal/stores/data_style/model/blocks/edges.ts create mode 100644 internal/stores/data_style/model/edges/component.ts create mode 100644 internal/stores/data_style/model/edges/width.ts create mode 100644 internal/stores/data_style/model/surfaces/edges.ts create mode 100644 tests/integration/stores/data_style/model/blocks_edges.nuxt.test.ts create mode 100644 tests/integration/stores/data_style/model/surfaces_edges.nuxt.test.ts diff --git a/internal/stores/data_style/model/blocks/edges.ts b/internal/stores/data_style/model/blocks/edges.ts new file mode 100644 index 000000000..a6c6323d8 --- /dev/null +++ b/internal/stores/data_style/model/blocks/edges.ts @@ -0,0 +1,51 @@ +import { + type ModelComponentEdgesStyle, + useModelComponentEdgesStyle, +} from "@ogw_internal/stores/data_style/model/edges/component"; + +interface UseModelBlocksEdgesStyleReturn { + modelBlockEdgesVisibility: ModelComponentEdgesStyle["visibility"]; + modelBlockEdgesWidth: ModelComponentEdgesStyle["width"]; + modelBlockEdgesColor: ModelComponentEdgesStyle["color"]; + modelBlockEdgesActiveColoring: ModelComponentEdgesStyle["activeColoring"]; + modelBlockEdgesVertexAttributeName: ModelComponentEdgesStyle["vertexAttributeName"]; + modelBlockEdgesVertexAttributeItem: ModelComponentEdgesStyle["vertexAttributeItem"]; + modelBlockEdgesVertexAttributeRange: ModelComponentEdgesStyle["vertexAttributeRange"]; + modelBlockEdgesVertexAttributeColorMap: ModelComponentEdgesStyle["vertexAttributeColorMap"]; + modelBlockEdgesVertexAttributeNoDataColor: ModelComponentEdgesStyle["vertexAttributeNoDataColor"]; + setModelBlocksEdgesVisibility: ModelComponentEdgesStyle["setVisibility"]; + setModelBlocksEdgesWidth: ModelComponentEdgesStyle["setWidth"]; + setModelBlocksEdgesColor: ModelComponentEdgesStyle["setColor"]; + setModelBlocksEdgesActiveColoring: ModelComponentEdgesStyle["setActiveColoring"]; + setModelBlocksEdgesVertexAttributeName: ModelComponentEdgesStyle["setVertexAttributeName"]; + setModelBlocksEdgesVertexAttributeItem: ModelComponentEdgesStyle["setVertexAttributeItem"]; + setModelBlocksEdgesVertexAttributeRange: ModelComponentEdgesStyle["setVertexAttributeRange"]; + setModelBlocksEdgesVertexAttributeColorMap: ModelComponentEdgesStyle["setVertexAttributeColorMap"]; + setModelBlocksEdgesVertexAttributeNoDataColor: ModelComponentEdgesStyle["setVertexAttributeNoDataColor"]; + applyModelBlocksEdgesStyle: ModelComponentEdgesStyle["applyEdgesStyle"]; +} + +export function useModelBlocksEdgesStyle(): UseModelBlocksEdgesStyleReturn { + const edges = useModelComponentEdgesStyle("Block"); + return { + modelBlockEdgesVisibility: edges.visibility, + modelBlockEdgesWidth: edges.width, + modelBlockEdgesColor: edges.color, + modelBlockEdgesActiveColoring: edges.activeColoring, + modelBlockEdgesVertexAttributeName: edges.vertexAttributeName, + modelBlockEdgesVertexAttributeItem: edges.vertexAttributeItem, + modelBlockEdgesVertexAttributeRange: edges.vertexAttributeRange, + modelBlockEdgesVertexAttributeColorMap: edges.vertexAttributeColorMap, + modelBlockEdgesVertexAttributeNoDataColor: edges.vertexAttributeNoDataColor, + setModelBlocksEdgesVisibility: edges.setVisibility, + setModelBlocksEdgesWidth: edges.setWidth, + setModelBlocksEdgesColor: edges.setColor, + setModelBlocksEdgesActiveColoring: edges.setActiveColoring, + setModelBlocksEdgesVertexAttributeName: edges.setVertexAttributeName, + setModelBlocksEdgesVertexAttributeItem: edges.setVertexAttributeItem, + setModelBlocksEdgesVertexAttributeRange: edges.setVertexAttributeRange, + setModelBlocksEdgesVertexAttributeColorMap: edges.setVertexAttributeColorMap, + setModelBlocksEdgesVertexAttributeNoDataColor: edges.setVertexAttributeNoDataColor, + applyModelBlocksEdgesStyle: edges.applyEdgesStyle, + }; +} diff --git a/internal/stores/data_style/model/blocks/index.ts b/internal/stores/data_style/model/blocks/index.ts index f7dfd01b9..6bbd48f71 100644 --- a/internal/stores/data_style/model/blocks/index.ts +++ b/internal/stores/data_style/model/blocks/index.ts @@ -7,6 +7,7 @@ import type { RGBAColor } from "@ogw_front/utils/default_styles/constants"; import { useDataStore } from "@ogw_front/stores/data"; import { useModelBlocksColor } from "./color"; import { useModelBlocksCommonStyle } from "./common"; +import { useModelBlocksEdgesStyle } from "./edges"; import { useModelBlocksPointsStyle } from "./points"; import { useModelBlocksVisibility } from "./visibility"; @@ -36,6 +37,7 @@ type UseModelBlocksStyleReturn = { ReturnType & ReturnType & ReturnType & + ReturnType & ReturnType & ReturnType; @@ -48,6 +50,7 @@ export function useModelBlocksStyle(): UseModelBlocksStyleReturn { const modelBlocksVertexAttribute = useModelBlocksVertexAttribute(); const modelBlocksPolyhedronAttribute = useModelBlocksPolyhedronAttribute(); const modelBlocksPointsStyle = useModelBlocksPointsStyle(); + const modelBlocksEdgesStyle = useModelBlocksEdgesStyle(); async function applyModelBlocksVisibilityStyle( modelId: string, @@ -249,5 +252,6 @@ export function useModelBlocksStyle(): UseModelBlocksStyleReturn { ...modelBlocksVertexAttribute, ...modelBlocksPolyhedronAttribute, ...modelBlocksPointsStyle, + ...modelBlocksEdgesStyle, }; } diff --git a/internal/stores/data_style/model/edges/component.ts b/internal/stores/data_style/model/edges/component.ts new file mode 100644 index 000000000..7bbd3eb20 --- /dev/null +++ b/internal/stores/data_style/model/edges/component.ts @@ -0,0 +1,406 @@ +// oxlint-disable eslint/max-lines +import { + type AttributeState, + type AttributeStoredConfig, + groupBy, +} from "@ogw_internal/stores/data_style/model/component_style"; +import { + DEFAULT_NO_DATA_COLOR, + EDGES_DEFAULT_COLOR, + EDGES_DEFAULT_VISIBILITY, + EDGES_DEFAULT_WIDTH, + type RGBAColor, +} from "@ogw_front/utils/default_styles/constants"; +import { getRGBPointsFromPreset } from "@ogw_front/utils/colormap"; +import merge from "lodash/merge"; +import { useAttributeTimeStepStyle } from "@ogw_internal/stores/data_style/time_step"; +import { useDataStore } from "@ogw_front/stores/data"; +import { useDataStyleState } from "@ogw_internal/stores/data_style/state"; +import { useModelCommonStyle } from "@ogw_internal/stores/data_style/model/common"; +import { useViewerStore } from "@ogw_front/stores/viewer"; +import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed_schemas.js"; + +// Lines are edges themselves and corners have none, so only these components get their own edges style +const EDGES_COMPONENT_TYPES = ["Surface", "Block"] as const; +type EdgesComponentType = (typeof EDGES_COMPONENT_TYPES)[number]; + +const modelSchemas = viewer_schemas.opengeodeweb_viewer.model; +const COMPONENT_TYPE_KEYS = { + Surface: "surfaces", + Block: "blocks", +} as const; + +type EdgesSchemas = + (typeof modelSchemas)[(typeof COMPONENT_TYPE_KEYS)[EdgesComponentType]]["edges"]; +type EdgesRequestSchema = + | EdgesSchemas["visibility"] + | EdgesSchemas["width"] + | EdgesSchemas["color"] + | EdgesSchemas["attribute"]["vertex"]["attribute"]; + +interface EdgesStyle { + visibility?: boolean; + width?: number; + coloring?: { + active?: string; + constant?: RGBAColor; + vertex?: AttributeState; + }; +} + +// Return type is inferred, see ModelComponentEdgesStyle: signatures live on the functions only +// oxlint-disable-next-line max-lines-per-function, max-statements, typescript/explicit-function-return-type, typescript/explicit-module-boundary-types +function useModelComponentEdgesStyle(type: EdgesComponentType) { + const dataStore = useDataStore(); + const dataStyleState = useDataStyleState(); + const modelCommonStyle = useModelCommonStyle(); + const viewerStore = useViewerStore(); + const { seriesArrayName } = useAttributeTimeStepStyle(); + const schemas = modelSchemas[COMPONENT_TYPE_KEYS[type]].edges; + + // Without an explicit edges style, components follow the model edges style + function edgesStyle(modelId: string, componentId?: string): EdgesStyle { + // oxlint-disable-next-line no-unsafe-type-assertion -- edges shape is defined by the data style schema. + const modelEdges = (dataStyleState.getStyle(modelId).edges ?? {}) as EdgesStyle; + // oxlint-disable-next-line no-unsafe-type-assertion -- edges shape is defined by the data style schema. + const typeEdges = dataStyleState.getModelComponentTypeStyle(modelId, type).edges as + | EdgesStyle + | undefined; + const componentEdges = + componentId === undefined + ? undefined + : // oxlint-disable-next-line no-unsafe-type-assertion -- edges shape is defined by the data style schema. + (dataStyleState.getComponentStyle(modelId, componentId).edges as EdgesStyle | undefined); + return merge( + { + visibility: modelEdges.visibility ?? EDGES_DEFAULT_VISIBILITY, + width: modelEdges.width ?? EDGES_DEFAULT_WIDTH, + coloring: { active: "constant", constant: EDGES_DEFAULT_COLOR }, + }, + typeEdges, + componentEdges, + ); + } + + function vertexAttribute(modelId: string, componentId?: string): AttributeState { + return edgesStyle(modelId, componentId).coloring?.vertex ?? {}; + } + + function edgesVisibility(modelId: string, componentId?: string): boolean { + return edgesStyle(modelId, componentId).visibility ?? EDGES_DEFAULT_VISIBILITY; + } + + function edgesWidth(modelId: string, componentId?: string): number { + return edgesStyle(modelId, componentId).width ?? EDGES_DEFAULT_WIDTH; + } + + function edgesColor(modelId: string, componentId?: string): RGBAColor { + return edgesStyle(modelId, componentId).coloring?.constant ?? EDGES_DEFAULT_COLOR; + } + + function activeColoring(modelId: string, componentId?: string): string { + return edgesStyle(modelId, componentId).coloring?.active ?? "constant"; + } + + function vertexAttributeName(modelId: string, componentId?: string): string | undefined { + return vertexAttribute(modelId, componentId).name; + } + + function lastItem(modelId: string, componentId: string | undefined, name?: string): number { + const { storedConfigs } = vertexAttribute(modelId, componentId); + return name === undefined ? 0 : (storedConfigs?.[name]?.lastItem ?? 0); + } + + function vertexAttributeItem(modelId: string, componentId?: string): number { + const { name, item } = vertexAttribute(modelId, componentId); + return item ?? lastItem(modelId, componentId, name); + } + + function storedConfig(modelId: string, componentId: string | undefined): AttributeStoredConfig { + const { name, storedConfigs } = vertexAttribute(modelId, componentId); + const item = vertexAttributeItem(modelId, componentId); + const config = name === undefined ? undefined : storedConfigs?.[name]?.[item]; + // A stored config may be partial, missing fields fall back to the defaults + return { + minimum: undefined, + maximum: undefined, + colorMap: undefined, + no_data_color: DEFAULT_NO_DATA_COLOR, + ...config, + }; + } + + function vertexAttributeRange( + modelId: string, + componentId?: string, + ): [number | undefined, number | undefined] { + const { minimum, maximum } = storedConfig(modelId, componentId); + return [minimum, maximum]; + } + + function vertexAttributeColorMap(modelId: string, componentId?: string): string | undefined { + return storedConfig(modelId, componentId).colorMap; + } + + function vertexAttributeNoDataColor(modelId: string, componentId?: string): RGBAColor { + return storedConfig(modelId, componentId).no_data_color; + } + + // The type style is updated too when every component of the type is targeted + async function mutateEdges( + modelId: string, + componentIds: string[], + values: EdgesStyle, + ): Promise { + const tasks = [ + modelCommonStyle.mutateComponentStyles(modelId, componentIds, { edges: values }), + ]; + const totalIds = await dataStore.getMeshComponentGeodeIds(modelId, type); + if (componentIds.length === totalIds.length) { + tasks.push(modelCommonStyle.mutateModelComponentTypeStyle(modelId, type, { edges: values })); + } + await Promise.all(tasks); + } + + async function requestEdges( + modelId: string, + componentIds: string[], + schema: EdgesRequestSchema, + values: Record & { style: EdgesStyle }, + ): Promise { + if (componentIds.length === 0) { + return undefined; + } + const block_ids = await dataStore.getMeshComponentsViewerIds(modelId, componentIds); + if (!block_ids?.length) { + return undefined; + } + const { style, ...params } = values; + const result = await viewerStore.request( + // oxlint-disable-next-line no-unsafe-type-assertion, no-unnecessary-type-assertion -- params match the edges schema of every component type. + { schema, params: { id: modelId, block_ids, ...params } } as Parameters< + typeof viewerStore.request + >[0], + { + response_function: async () => { + await mutateEdges(modelId, componentIds, style); + }, + }, + ); + return result; + } + + async function setVisibility( + modelId: string, + componentIds: string[], + visibility: boolean, + ): Promise { + const result = await requestEdges(modelId, componentIds, schemas.visibility, { + visibility, + style: { visibility }, + }); + return result; + } + + async function setWidth( + modelId: string, + componentIds: string[], + width: number, + ): Promise { + const result = await requestEdges(modelId, componentIds, schemas.width, { + width, + style: { width }, + }); + return result; + } + + async function setColor( + modelId: string, + componentIds: string[], + color: RGBAColor, + ): Promise { + const result = await requestEdges(modelId, componentIds, schemas.color, { + color, + style: { coloring: { active: "constant", constant: color } }, + }); + return result; + } + + async function applyVertexAttribute(modelId: string, componentIds: string[]): Promise { + const [referenceId] = componentIds; + const name = vertexAttributeName(modelId, referenceId); + const item = vertexAttributeItem(modelId, referenceId); + const { minimum, maximum, colorMap, no_data_color } = storedConfig(modelId, referenceId); + if ( + name === undefined || + minimum === undefined || + maximum === undefined || + colorMap === undefined + ) { + return undefined; + } + const schema = schemas.attribute.vertex.attribute; + const result = await requestEdges(modelId, componentIds, schema, { + name: seriesArrayName(modelId, schema.$id, name), + item, + points: [...getRGBPointsFromPreset(colorMap)], + minimum, + maximum, + no_data_color, + style: { coloring: { active: "vertex" } }, + }); + return result; + } + + async function mutateVertexAttribute( + modelId: string, + componentIds: string[], + values: AttributeState, + ): Promise { + await mutateEdges(modelId, componentIds, { coloring: { vertex: values } }); + } + + async function mutateStoredConfig( + modelId: string, + componentIds: string[], + config: Partial, + ): Promise { + const name = vertexAttributeName(modelId, componentIds[0]); + const item = vertexAttributeItem(modelId, componentIds[0]); + const itemConfigs = { lastItem: item, [item]: config }; + // oxlint-disable-next-line no-unsafe-type-assertion -- partial stored config is merged into the existing one. + const storedConfigs = { [name ?? ""]: itemConfigs } as AttributeState["storedConfigs"]; + await mutateVertexAttribute(modelId, componentIds, { storedConfigs }); + } + + async function setActiveColoring( + modelId: string, + componentIds: string[], + activeColoringType: string, + ): Promise { + await mutateEdges(modelId, componentIds, { coloring: { active: activeColoringType } }); + if (activeColoringType === "vertex") { + const result = await applyVertexAttribute(modelId, componentIds); + return result; + } + const result = await setColor(modelId, componentIds, edgesColor(modelId, componentIds[0])); + return result; + } + + async function setVertexAttributeName( + modelId: string, + componentIds: string[], + name: string, + ): Promise { + const item = lastItem(modelId, componentIds[0], name); + await mutateVertexAttribute(modelId, componentIds, { name, item }); + const result = await applyVertexAttribute(modelId, componentIds); + return result; + } + + async function setVertexAttributeItem( + modelId: string, + componentIds: string[], + item: number, + ): Promise { + await mutateVertexAttribute(modelId, componentIds, { item }); + const result = await applyVertexAttribute(modelId, componentIds); + return result; + } + + async function setVertexAttributeRange( + modelId: string, + componentIds: string[], + minimum: number, + maximum: number, + ): Promise { + await mutateStoredConfig(modelId, componentIds, { minimum, maximum }); + const result = await applyVertexAttribute(modelId, componentIds); + return result; + } + + async function setVertexAttributeColorMap( + modelId: string, + componentIds: string[], + colorMap: string | undefined, + ): Promise { + await mutateStoredConfig(modelId, componentIds, { colorMap }); + const result = await applyVertexAttribute(modelId, componentIds); + return result; + } + + async function setVertexAttributeNoDataColor( + modelId: string, + componentIds: string[], + no_data_color: RGBAColor, + ): Promise { + await mutateStoredConfig(modelId, componentIds, { no_data_color }); + const result = await applyVertexAttribute(modelId, componentIds); + return result; + } + + async function styledComponentIds(modelId: string): Promise { + const componentIds = await dataStore.getMeshComponentGeodeIds(modelId, type); + if (dataStyleState.getModelComponentTypeStyle(modelId, type).edges !== undefined) { + return componentIds; + } + return componentIds.filter( + (componentId) => dataStyleState.getComponentStyle(modelId, componentId).edges !== undefined, + ); + } + + // The model edges routes override every component, so the explicit edges styles follow + async function mutateStyledEdges(modelId: string, values: EdgesStyle): Promise { + const componentIds = await styledComponentIds(modelId); + if (componentIds.length > 0) { + await mutateEdges(modelId, componentIds, values); + } + } + + // Only components with an explicit edges style are sent, the others follow the model edges + async function applyEdgesStyle(modelId: string): Promise { + const componentIds = await styledComponentIds(modelId); + function style(componentId?: string): EdgesStyle { + return edgesStyle(modelId, componentId); + } + await Promise.all([ + ...groupBy(componentIds, (id) => String(style(id).visibility)).map(async (ids) => { + await setVisibility(modelId, ids, edgesVisibility(modelId, ids[0])); + }), + ...groupBy(componentIds, (id) => String(style(id).width)).map(async (ids) => { + await setWidth(modelId, ids, edgesWidth(modelId, ids[0])); + }), + ...groupBy(componentIds, (id) => JSON.stringify(style(id).coloring)).map(async (ids) => { + await setActiveColoring(modelId, ids, activeColoring(modelId, ids[0])); + }), + ]); + } + + return { + visibility: edgesVisibility, + width: edgesWidth, + color: edgesColor, + activeColoring, + vertexAttributeName, + vertexAttributeItem, + vertexAttributeRange, + vertexAttributeColorMap, + vertexAttributeNoDataColor, + setVisibility, + setWidth, + setColor, + setActiveColoring, + setVertexAttributeName, + setVertexAttributeItem, + setVertexAttributeRange, + setVertexAttributeColorMap, + setVertexAttributeNoDataColor, + mutateStyledEdges, + applyEdgesStyle, + }; +} + +type ModelComponentEdgesStyle = ReturnType; + +export { EDGES_COMPONENT_TYPES, useModelComponentEdgesStyle }; +export type { ModelComponentEdgesStyle }; diff --git a/internal/stores/data_style/model/edges/index.ts b/internal/stores/data_style/model/edges/index.ts index fc637d90d..91c692b0f 100644 --- a/internal/stores/data_style/model/edges/index.ts +++ b/internal/stores/data_style/model/edges/index.ts @@ -1,13 +1,17 @@ import { useModelEdgesCommonStyle } from "./common"; import { useModelEdgesVisibilityStyle } from "./visibility"; +import { useModelEdgesWidthStyle } from "./width"; export function useModelEdgesStyle(): ReturnType & - ReturnType { + ReturnType & + ReturnType { const modelEdgesCommonStyle = useModelEdgesCommonStyle(); const modelEdgesVisibilityStyle = useModelEdgesVisibilityStyle(); + const modelEdgesWidthStyle = useModelEdgesWidthStyle(); return { ...modelEdgesCommonStyle, ...modelEdgesVisibilityStyle, + ...modelEdgesWidthStyle, }; } diff --git a/internal/stores/data_style/model/edges/visibility.ts b/internal/stores/data_style/model/edges/visibility.ts index 32e08656e..e0641f9cd 100644 --- a/internal/stores/data_style/model/edges/visibility.ts +++ b/internal/stores/data_style/model/edges/visibility.ts @@ -1,7 +1,12 @@ // Third party imports import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed_schemas.js"; +import { + EDGES_COMPONENT_TYPES, + useModelComponentEdgesStyle, +} from "@ogw_internal/stores/data_style/model/edges/component"; import { useModelEdgesCommonStyle } from "./common"; +import { useModelEdgesWidthStyle } from "./width"; import { useViewerStore } from "@ogw_front/stores/viewer"; import { whenDefined } from "@ogw_internal/stores/data_style/when_defined"; @@ -17,6 +22,10 @@ interface ModelEdgesVisibilityStyleApi { export function useModelEdgesVisibilityStyle(): ModelEdgesVisibilityStyleApi { const viewerStore = useViewerStore(); const modelEdgesCommonStyle = useModelEdgesCommonStyle(); + const modelEdgesWidthStyle = useModelEdgesWidthStyle(); + const componentsEdgesStyles = EDGES_COMPONENT_TYPES.map((type) => + useModelComponentEdgesStyle(type), + ); function modelEdgesVisibility(id: string): boolean | undefined { const { visibility } = modelEdgesCommonStyle.modelEdgesStyle(id); @@ -32,7 +41,12 @@ export function useModelEdgesVisibilityStyle(): ModelEdgesVisibilityStyleApi { { schema, params }, { response_function: async () => { - await modelEdgesCommonStyle.mutateModelEdgesStyle(id, { visibility }); + await Promise.all([ + modelEdgesCommonStyle.mutateModelEdgesStyle(id, { visibility }), + ...componentsEdgesStyles.map(async (edgesStyle) => { + await edgesStyle.mutateStyledEdges(id, { visibility }); + }), + ]); }, }, ); @@ -40,11 +54,18 @@ export function useModelEdgesVisibilityStyle(): ModelEdgesVisibilityStyleApi { } async function applyModelEdgesStyle(id: string): Promise { - const result = await whenDefined(modelEdgesVisibility(id), async (visibility) => { + const visibilityResult = await whenDefined(modelEdgesVisibility(id), async (visibility) => { const applied = await setModelEdgesVisibility(id, visibility); return applied; }); - return [result]; + const widthResult = await whenDefined( + modelEdgesWidthStyle.modelEdgesWidth(id), + async (width) => { + const applied = await modelEdgesWidthStyle.setModelEdgesWidth(id, width); + return applied; + }, + ); + return [visibilityResult, widthResult]; } return { diff --git a/internal/stores/data_style/model/edges/width.ts b/internal/stores/data_style/model/edges/width.ts new file mode 100644 index 000000000..9b1bcd5e1 --- /dev/null +++ b/internal/stores/data_style/model/edges/width.ts @@ -0,0 +1,56 @@ +// Third party imports +import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed_schemas.js"; + +// Local imports +import { + EDGES_COMPONENT_TYPES, + useModelComponentEdgesStyle, +} from "@ogw_internal/stores/data_style/model/edges/component"; +import { useModelEdgesCommonStyle } from "./common"; +import { useViewerStore } from "@ogw_front/stores/viewer"; + +// Local constants +const schema = viewer_schemas.opengeodeweb_viewer.model.edges.width; + +export function useModelEdgesWidthStyle(): { + modelEdgesWidth: (id: string) => number | undefined; + setModelEdgesWidth: (id: string, width: number) => Promise; +} { + const viewerStore = useViewerStore(); + const modelEdgesCommonStyle = useModelEdgesCommonStyle(); + const componentsEdgesStyles = EDGES_COMPONENT_TYPES.map((type) => + useModelComponentEdgesStyle(type), + ); + + function modelEdgesWidth(id: string): number | undefined { + // oxlint-disable-next-line no-unsafe-type-assertion -- edges style values are dynamically typed at runtime. + return modelEdgesCommonStyle.modelEdgesStyle(id).width as number | undefined; + } + + async function setModelEdgesWidth(id: string, width: number): Promise { + const params = { id, width }; + const result = await viewerStore.request( + { + schema, + params, + }, + { + response_function: async () => { + const [mutateResult] = await Promise.all([ + modelEdgesCommonStyle.mutateModelEdgesStyle(id, { width }), + ...componentsEdgesStyles.map(async (edgesStyle) => { + await edgesStyle.mutateStyledEdges(id, { width }); + }), + ]); + return mutateResult; + }, + }, + ); + return result; + } + + return { + modelEdgesWidth, + setModelEdgesWidth, + }; +} diff --git a/internal/stores/data_style/model/index.ts b/internal/stores/data_style/model/index.ts index bfa2ce5e2..50156f3c5 100644 --- a/internal/stores/data_style/model/index.ts +++ b/internal/stores/data_style/model/index.ts @@ -61,11 +61,13 @@ function useModelStyle(): UseModelStyleReturn { modelPointsStyle.applyModelPointsStyle(modelId), modelEdgesStyle.applyModelEdgesStyle(modelId), ]); - // Components points come after the model points, which override every component + // Components points and edges come after the model ones, which override every component await Promise.all([ modelSurfacesStyle.applyModelSurfacesPointsStyle(modelId), modelLinesStyle.applyModelLinesPointsStyle(modelId), modelBlocksStyle.applyModelBlocksPointsStyle(modelId), + modelSurfacesStyle.applyModelSurfacesEdgesStyle(modelId), + modelBlocksStyle.applyModelBlocksEdgesStyle(modelId), ]); return results; } diff --git a/internal/stores/data_style/model/surfaces/edges.ts b/internal/stores/data_style/model/surfaces/edges.ts new file mode 100644 index 000000000..9117f57c1 --- /dev/null +++ b/internal/stores/data_style/model/surfaces/edges.ts @@ -0,0 +1,51 @@ +import { + type ModelComponentEdgesStyle, + useModelComponentEdgesStyle, +} from "@ogw_internal/stores/data_style/model/edges/component"; + +interface UseModelSurfacesEdgesStyleReturn { + modelSurfaceEdgesVisibility: ModelComponentEdgesStyle["visibility"]; + modelSurfaceEdgesWidth: ModelComponentEdgesStyle["width"]; + modelSurfaceEdgesColor: ModelComponentEdgesStyle["color"]; + modelSurfaceEdgesActiveColoring: ModelComponentEdgesStyle["activeColoring"]; + modelSurfaceEdgesVertexAttributeName: ModelComponentEdgesStyle["vertexAttributeName"]; + modelSurfaceEdgesVertexAttributeItem: ModelComponentEdgesStyle["vertexAttributeItem"]; + modelSurfaceEdgesVertexAttributeRange: ModelComponentEdgesStyle["vertexAttributeRange"]; + modelSurfaceEdgesVertexAttributeColorMap: ModelComponentEdgesStyle["vertexAttributeColorMap"]; + modelSurfaceEdgesVertexAttributeNoDataColor: ModelComponentEdgesStyle["vertexAttributeNoDataColor"]; + setModelSurfacesEdgesVisibility: ModelComponentEdgesStyle["setVisibility"]; + setModelSurfacesEdgesWidth: ModelComponentEdgesStyle["setWidth"]; + setModelSurfacesEdgesColor: ModelComponentEdgesStyle["setColor"]; + setModelSurfacesEdgesActiveColoring: ModelComponentEdgesStyle["setActiveColoring"]; + setModelSurfacesEdgesVertexAttributeName: ModelComponentEdgesStyle["setVertexAttributeName"]; + setModelSurfacesEdgesVertexAttributeItem: ModelComponentEdgesStyle["setVertexAttributeItem"]; + setModelSurfacesEdgesVertexAttributeRange: ModelComponentEdgesStyle["setVertexAttributeRange"]; + setModelSurfacesEdgesVertexAttributeColorMap: ModelComponentEdgesStyle["setVertexAttributeColorMap"]; + setModelSurfacesEdgesVertexAttributeNoDataColor: ModelComponentEdgesStyle["setVertexAttributeNoDataColor"]; + applyModelSurfacesEdgesStyle: ModelComponentEdgesStyle["applyEdgesStyle"]; +} + +export function useModelSurfacesEdgesStyle(): UseModelSurfacesEdgesStyleReturn { + const edges = useModelComponentEdgesStyle("Surface"); + return { + modelSurfaceEdgesVisibility: edges.visibility, + modelSurfaceEdgesWidth: edges.width, + modelSurfaceEdgesColor: edges.color, + modelSurfaceEdgesActiveColoring: edges.activeColoring, + modelSurfaceEdgesVertexAttributeName: edges.vertexAttributeName, + modelSurfaceEdgesVertexAttributeItem: edges.vertexAttributeItem, + modelSurfaceEdgesVertexAttributeRange: edges.vertexAttributeRange, + modelSurfaceEdgesVertexAttributeColorMap: edges.vertexAttributeColorMap, + modelSurfaceEdgesVertexAttributeNoDataColor: edges.vertexAttributeNoDataColor, + setModelSurfacesEdgesVisibility: edges.setVisibility, + setModelSurfacesEdgesWidth: edges.setWidth, + setModelSurfacesEdgesColor: edges.setColor, + setModelSurfacesEdgesActiveColoring: edges.setActiveColoring, + setModelSurfacesEdgesVertexAttributeName: edges.setVertexAttributeName, + setModelSurfacesEdgesVertexAttributeItem: edges.setVertexAttributeItem, + setModelSurfacesEdgesVertexAttributeRange: edges.setVertexAttributeRange, + setModelSurfacesEdgesVertexAttributeColorMap: edges.setVertexAttributeColorMap, + setModelSurfacesEdgesVertexAttributeNoDataColor: edges.setVertexAttributeNoDataColor, + applyModelSurfacesEdgesStyle: edges.applyEdgesStyle, + }; +} diff --git a/internal/stores/data_style/model/surfaces/index.ts b/internal/stores/data_style/model/surfaces/index.ts index ef55e0385..b9ae7e593 100644 --- a/internal/stores/data_style/model/surfaces/index.ts +++ b/internal/stores/data_style/model/surfaces/index.ts @@ -4,6 +4,7 @@ import type { RGBAColor } from "@ogw_front/utils/default_styles/constants"; import { useDataStore } from "@ogw_front/stores/data"; import { useModelSurfacesColor } from "./color"; import { useModelSurfacesCommonStyle } from "./common"; +import { useModelSurfacesEdgesStyle } from "./edges"; import { useModelSurfacesPointsStyle } from "./points"; import { useModelSurfacesVisibility } from "./visibility"; @@ -30,6 +31,7 @@ type UseModelSurfacesStyleReturn = ReturnType & ReturnType & ReturnType & + ReturnType & ReturnType & ReturnType & { applyModelSurfacesStyle: (modelId: string) => Promise; @@ -45,6 +47,7 @@ export function useModelSurfacesStyle(): UseModelSurfacesStyleReturn { const modelSurfacesVertexAttribute = useModelSurfacesVertexAttribute(); const modelSurfacesPolygonAttribute = useModelSurfacesPolygonAttribute(); const modelSurfacesPointsStyle = useModelSurfacesPointsStyle(); + const modelSurfacesEdgesStyle = useModelSurfacesEdgesStyle(); async function applyModelSurfacesVisibilityStyle( modelId: string, @@ -268,5 +271,6 @@ export function useModelSurfacesStyle(): UseModelSurfacesStyleReturn { ...modelSurfacesVertexAttribute, ...modelSurfacesPolygonAttribute, ...modelSurfacesPointsStyle, + ...modelSurfacesEdgesStyle, }; } diff --git a/tests/integration/stores/data_style/model/blocks_edges.nuxt.test.ts b/tests/integration/stores/data_style/model/blocks_edges.nuxt.test.ts new file mode 100644 index 000000000..98b13fdab --- /dev/null +++ b/tests/integration/stores/data_style/model/blocks_edges.nuxt.test.ts @@ -0,0 +1,168 @@ +// Third party imports +import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; +import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed_schemas.js"; + +// Local imports +import { beforeAllTimeout, setupIntegrationTests } from "@ogw_tests/integration/setup"; +import { DEFAULT_NO_DATA_COLOR } from "@ogw_front/utils/default_styles/constants"; +import { Status } from "@ogw_front/utils/status"; +import { cleanupBackend } from "@ogw_server/utils/cleanup"; +import { getRGBPointsFromPreset } from "@ogw_front/utils/colormap"; +import { useDataStore } from "@ogw_front/stores/data"; +import { useDataStyleStore } from "@ogw_front/stores/data_style"; +import { useViewerStore } from "@ogw_front/stores/viewer"; + +// Local constants +const model_blocks_edges_schemas = viewer_schemas.opengeodeweb_viewer.model.blocks.edges; +const file_name = "test.og_brep"; +const geode_object = "BRep"; +const SLEEP_MS = 200; +const EDGES_WIDTH = 5; +const MINIMUM_RANGE = 10; +const MAXIMUM_RANGE = 20; +const TEST_TIMEOUT_MS = 15_000; + +async function sleep(milliseconds: number): Promise { + // oxlint-disable-next-line promise/avoid-new + await new Promise((resolve) => { + setTimeout(resolve, milliseconds); + }); +} + +function firstElement(array: readonly Item[]): Item { + const [element] = array; + if (element === undefined) { + throw new Error("Expected array to contain at least one element"); + } + return element; +} + +let id = ""; +let projectFolderPath = ""; + +describe("model blocks edges", () => { + beforeAll(async () => { + ({ id, projectFolderPath } = await setupIntegrationTests(file_name, geode_object)); + }, beforeAllTimeout); + + afterAll(async () => { + await cleanupBackend(projectFolderPath); + }); + + test( + "blocks edges visibility and width", + async () => { + const dataStyleStore = useDataStyleStore(); + const viewerStore = useViewerStore(); + const dataStore = useDataStore(); + const block_geode_ids = await dataStore.getBlocksGeodeIds(id); + const block_ids = await dataStore.getMeshComponentsViewerIds(id, block_geode_ids); + const spy = vi.spyOn(viewerStore, "request"); + spy.mockClear(); + await dataStyleStore.setModelBlocksEdgesVisibility(id, block_geode_ids, true); + await dataStyleStore.setModelBlocksEdgesWidth(id, block_geode_ids, EDGES_WIDTH); + await sleep(SLEEP_MS); + expect(spy).toHaveBeenCalledWith( + { + schema: model_blocks_edges_schemas.visibility, + params: { id, block_ids, visibility: true }, + }, + { + // oxlint-disable-next-line no-unsafe-assignment -- expect.any(Function) is untyped by design, this is a vitest matcher not a real callback. + response_function: expect.any(Function), + }, + ); + expect(spy).toHaveBeenCalledWith( + { + schema: model_blocks_edges_schemas.width, + params: { id, block_ids, width: EDGES_WIDTH }, + }, + { + // oxlint-disable-next-line no-unsafe-assignment -- expect.any(Function) is untyped by design, this is a vitest matcher not a real callback. + response_function: expect.any(Function), + }, + ); + for (const block_geode_id of block_geode_ids) { + expect(dataStyleStore.modelBlockEdgesVisibility(id, block_geode_id)).toBe(true); + expect(dataStyleStore.modelBlockEdgesWidth(id, block_geode_id)).toBe(EDGES_WIDTH); + } + expect(viewerStore.status).toBe(Status.CONNECTED); + }, + TEST_TIMEOUT_MS, + ); + + test( + "single block edges color with opacity", + async () => { + const dataStyleStore = useDataStyleStore(); + const viewerStore = useViewerStore(); + const dataStore = useDataStore(); + const block_geode_id = firstElement(await dataStore.getBlocksGeodeIds(id)); + const block_ids = await dataStore.getMeshComponentsViewerIds(id, [block_geode_id]); + const color = { red: 255, green: 0, blue: 0, alpha: 0.5 }; + const spy = vi.spyOn(viewerStore, "request"); + spy.mockClear(); + await dataStyleStore.setModelBlocksEdgesColor(id, [block_geode_id], color); + await sleep(SLEEP_MS); + expect(spy).toHaveBeenCalledWith( + { schema: model_blocks_edges_schemas.color, params: { id, block_ids, color } }, + { + // oxlint-disable-next-line no-unsafe-assignment -- expect.any(Function) is untyped by design, this is a vitest matcher not a real callback. + response_function: expect.any(Function), + }, + ); + expect(dataStyleStore.modelBlockEdgesColor(id, block_geode_id)).toStrictEqual(color); + expect(dataStyleStore.modelBlockEdgesActiveColoring(id, block_geode_id)).toBe("constant"); + }, + TEST_TIMEOUT_MS, + ); + + test( + "blocks edges vertex attribute", + async () => { + const dataStyleStore = useDataStyleStore(); + const viewerStore = useViewerStore(); + const dataStore = useDataStore(); + const block_geode_ids = await dataStore.getBlocksGeodeIds(id); + const block_ids = await dataStore.getMeshComponentsViewerIds(id, block_geode_ids); + const name = "points"; + const colorMap = "batlow"; + await dataStyleStore.setModelBlocksEdgesVertexAttributeName(id, block_geode_ids, name); + await dataStyleStore.setModelBlocksEdgesVertexAttributeColorMap( + id, + block_geode_ids, + colorMap, + ); + const spy = vi.spyOn(viewerStore, "request"); + spy.mockClear(); + await dataStyleStore.setModelBlocksEdgesVertexAttributeRange( + id, + block_geode_ids, + MINIMUM_RANGE, + MAXIMUM_RANGE, + ); + await sleep(SLEEP_MS); + const params = { + id, + block_ids, + name, + item: 0, + points: [...getRGBPointsFromPreset(colorMap)], + minimum: MINIMUM_RANGE, + maximum: MAXIMUM_RANGE, + no_data_color: DEFAULT_NO_DATA_COLOR, + }; + expect(spy).toHaveBeenCalledWith( + { schema: model_blocks_edges_schemas.attribute.vertex.attribute, params }, + { + // oxlint-disable-next-line no-unsafe-assignment -- expect.any(Function) is untyped by design, this is a vitest matcher not a real callback. + response_function: expect.any(Function), + }, + ); + const block_geode_id = firstElement(block_geode_ids); + expect(dataStyleStore.modelBlockEdgesActiveColoring(id, block_geode_id)).toBe("vertex"); + expect(dataStyleStore.modelBlockEdgesVertexAttributeName(id, block_geode_id)).toBe(name); + }, + TEST_TIMEOUT_MS, + ); +}); diff --git a/tests/integration/stores/data_style/model/edges.nuxt.test.ts b/tests/integration/stores/data_style/model/edges.nuxt.test.ts index 4cf4f57f1..5637d655b 100644 --- a/tests/integration/stores/data_style/model/edges.nuxt.test.ts +++ b/tests/integration/stores/data_style/model/edges.nuxt.test.ts @@ -6,6 +6,7 @@ import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed import { beforeAllTimeout, setupIntegrationTests } from "@ogw_tests/integration/setup"; import { Status } from "@ogw_front/utils/status"; import { cleanupBackend } from "@ogw_server/utils/cleanup"; +import { useDataStore } from "@ogw_front/stores/data"; import { useDataStyleStore } from "@ogw_front/stores/data_style"; import { useViewerStore } from "@ogw_front/stores/viewer"; @@ -14,6 +15,8 @@ const model_edges_schemas = viewer_schemas.opengeodeweb_viewer.model.edges; const file_name = "test.og_brep"; const geode_object = "BRep"; const SLEEP_MS = 200; +const COMPONENT_EDGES_WIDTH = 8; +const MODEL_EDGES_WIDTH = 1; async function sleep(milliseconds: number): Promise { // oxlint-disable-next-line promise/avoid-new @@ -58,6 +61,31 @@ describe("model edges", () => { expect(viewerStore.status).toBe(Status.CONNECTED); }); }); + describe("edges width", () => { + test("model edges width overrides components", async () => { + const dataStyleStore = useDataStyleStore(); + const viewerStore = useViewerStore(); + const dataStore = useDataStore(); + const [surface_id] = await dataStore.getSurfacesGeodeIds(id); + if (surface_id === undefined) { + throw new Error("Expected the model to contain a surface"); + } + await dataStyleStore.setModelSurfacesEdgesWidth(id, [surface_id], COMPONENT_EDGES_WIDTH); + const spy = vi.spyOn(viewerStore, "request"); + spy.mockClear(); + await dataStyleStore.setModelEdgesWidth(id, MODEL_EDGES_WIDTH); + await sleep(SLEEP_MS); + expect(spy).toHaveBeenCalledWith( + { schema: model_edges_schemas.width, params: { id, width: MODEL_EDGES_WIDTH } }, + { + // oxlint-disable-next-line no-unsafe-assignment -- expect.any(Function) is untyped by design, this is a vitest matcher not a real callback. + response_function: expect.any(Function), + }, + ); + expect(dataStyleStore.modelEdgesWidth(id)).toBe(MODEL_EDGES_WIDTH); + expect(dataStyleStore.modelSurfaceEdgesWidth(id, surface_id)).toBe(MODEL_EDGES_WIDTH); + }); + }); describe("edges style", () => { test("edges apply style", async () => { const dataStyleStore = useDataStyleStore(); diff --git a/tests/integration/stores/data_style/model/surfaces_edges.nuxt.test.ts b/tests/integration/stores/data_style/model/surfaces_edges.nuxt.test.ts new file mode 100644 index 000000000..948590246 --- /dev/null +++ b/tests/integration/stores/data_style/model/surfaces_edges.nuxt.test.ts @@ -0,0 +1,177 @@ +// Third party imports +import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; +import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed_schemas.js"; + +// Local imports +import { beforeAllTimeout, setupIntegrationTests } from "@ogw_tests/integration/setup"; +import { DEFAULT_NO_DATA_COLOR } from "@ogw_front/utils/default_styles/constants"; +import { Status } from "@ogw_front/utils/status"; +import { cleanupBackend } from "@ogw_server/utils/cleanup"; +import { getRGBPointsFromPreset } from "@ogw_front/utils/colormap"; +import { useDataStore } from "@ogw_front/stores/data"; +import { useDataStyleStore } from "@ogw_front/stores/data_style"; +import { useViewerStore } from "@ogw_front/stores/viewer"; + +// Local constants +const model_surfaces_edges_schemas = viewer_schemas.opengeodeweb_viewer.model.surfaces.edges; +const file_name = "test.og_brep"; +const geode_object = "BRep"; +const SLEEP_MS = 200; +const EDGES_WIDTH = 5; +const MINIMUM_RANGE = 10; +const MAXIMUM_RANGE = 20; +const TEST_TIMEOUT_MS = 15_000; + +async function sleep(milliseconds: number): Promise { + // oxlint-disable-next-line promise/avoid-new + await new Promise((resolve) => { + setTimeout(resolve, milliseconds); + }); +} + +function firstElement(array: readonly Item[]): Item { + const [element] = array; + if (element === undefined) { + throw new Error("Expected array to contain at least one element"); + } + return element; +} + +let id = ""; +let projectFolderPath = ""; + +describe("model surfaces edges", () => { + beforeAll(async () => { + ({ id, projectFolderPath } = await setupIntegrationTests(file_name, geode_object)); + }, beforeAllTimeout); + + afterAll(async () => { + await cleanupBackend(projectFolderPath); + }); + + test( + "surfaces edges visibility and width", + async () => { + const dataStyleStore = useDataStyleStore(); + const viewerStore = useViewerStore(); + const dataStore = useDataStore(); + const surface_ids = await dataStore.getSurfacesGeodeIds(id); + const block_ids = await dataStore.getMeshComponentsViewerIds(id, surface_ids); + const spy = vi.spyOn(viewerStore, "request"); + spy.mockClear(); + await dataStyleStore.setModelSurfacesEdgesVisibility(id, surface_ids, true); + await dataStyleStore.setModelSurfacesEdgesWidth(id, surface_ids, EDGES_WIDTH); + await sleep(SLEEP_MS); + expect(spy).toHaveBeenCalledWith( + { + schema: model_surfaces_edges_schemas.visibility, + params: { id, block_ids, visibility: true }, + }, + { + // oxlint-disable-next-line no-unsafe-assignment -- expect.any(Function) is untyped by design, this is a vitest matcher not a real callback. + response_function: expect.any(Function), + }, + ); + expect(spy).toHaveBeenCalledWith( + { + schema: model_surfaces_edges_schemas.width, + params: { id, block_ids, width: EDGES_WIDTH }, + }, + { + // oxlint-disable-next-line no-unsafe-assignment -- expect.any(Function) is untyped by design, this is a vitest matcher not a real callback. + response_function: expect.any(Function), + }, + ); + for (const surface_id of surface_ids) { + expect(dataStyleStore.modelSurfaceEdgesVisibility(id, surface_id)).toBe(true); + expect(dataStyleStore.modelSurfaceEdgesWidth(id, surface_id)).toBe(EDGES_WIDTH); + } + expect(viewerStore.status).toBe(Status.CONNECTED); + }, + TEST_TIMEOUT_MS, + ); + + test( + "single surface edges color with opacity", + async () => { + const dataStyleStore = useDataStyleStore(); + const viewerStore = useViewerStore(); + const dataStore = useDataStore(); + const surface_id = firstElement(await dataStore.getSurfacesGeodeIds(id)); + const block_ids = await dataStore.getMeshComponentsViewerIds(id, [surface_id]); + const color = { red: 255, green: 0, blue: 0, alpha: 0.5 }; + const spy = vi.spyOn(viewerStore, "request"); + spy.mockClear(); + await dataStyleStore.setModelSurfacesEdgesColor(id, [surface_id], color); + await sleep(SLEEP_MS); + expect(spy).toHaveBeenCalledWith( + { schema: model_surfaces_edges_schemas.color, params: { id, block_ids, color } }, + { + // oxlint-disable-next-line no-unsafe-assignment -- expect.any(Function) is untyped by design, this is a vitest matcher not a real callback. + response_function: expect.any(Function), + }, + ); + expect(dataStyleStore.modelSurfaceEdgesColor(id, surface_id)).toStrictEqual(color); + expect(dataStyleStore.modelSurfaceEdgesActiveColoring(id, surface_id)).toBe("constant"); + }, + TEST_TIMEOUT_MS, + ); + + test( + "surfaces edges vertex attribute", + async () => { + const dataStyleStore = useDataStyleStore(); + const viewerStore = useViewerStore(); + const dataStore = useDataStore(); + const surface_ids = await dataStore.getSurfacesGeodeIds(id); + const block_ids = await dataStore.getMeshComponentsViewerIds(id, surface_ids); + const name = "points"; + const colorMap = "batlow"; + await dataStyleStore.setModelSurfacesEdgesVertexAttributeName(id, surface_ids, name); + await dataStyleStore.setModelSurfacesEdgesVertexAttributeColorMap(id, surface_ids, colorMap); + const spy = vi.spyOn(viewerStore, "request"); + spy.mockClear(); + await dataStyleStore.setModelSurfacesEdgesVertexAttributeRange( + id, + surface_ids, + MINIMUM_RANGE, + MAXIMUM_RANGE, + ); + await sleep(SLEEP_MS); + const params = { + id, + block_ids, + name, + item: 0, + points: [...getRGBPointsFromPreset(colorMap)], + minimum: MINIMUM_RANGE, + maximum: MAXIMUM_RANGE, + no_data_color: DEFAULT_NO_DATA_COLOR, + }; + expect(spy).toHaveBeenCalledWith( + { schema: model_surfaces_edges_schemas.attribute.vertex.attribute, params }, + { + // oxlint-disable-next-line no-unsafe-assignment -- expect.any(Function) is untyped by design, this is a vitest matcher not a real callback. + response_function: expect.any(Function), + }, + ); + const surface_id = firstElement(surface_ids); + expect(dataStyleStore.modelSurfaceEdgesActiveColoring(id, surface_id)).toBe("vertex"); + expect(dataStyleStore.modelSurfaceEdgesVertexAttributeName(id, surface_id)).toBe(name); + }, + TEST_TIMEOUT_MS, + ); + + test( + "model edges visibility overrides components edges", + async () => { + const dataStyleStore = useDataStyleStore(); + const dataStore = useDataStore(); + const surface_id = firstElement(await dataStore.getSurfacesGeodeIds(id)); + await dataStyleStore.setModelEdgesVisibility(id, false); + await sleep(SLEEP_MS); + expect(dataStyleStore.modelSurfaceEdgesVisibility(id, surface_id)).toBe(false); + }, + TEST_TIMEOUT_MS, + ); +}); From a9633e52f21830f3f7fbf8620f1600960041a36e Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Wed, 21 Oct 2026 15:04:26 +0200 Subject: [PATCH 4/6] Add contextual model edges card --- .../Generic/Model/ComponentEdgesOptions.vue | 223 ++++++++++++++++++ .../Viewer/Generic/Model/EdgesOptions.vue | 29 +-- .../Viewer/Generic/Model/ModelEdgesCard.vue | 159 +++++++++++++ 3 files changed, 384 insertions(+), 27 deletions(-) create mode 100644 app/components/Viewer/Generic/Model/ComponentEdgesOptions.vue create mode 100644 app/components/Viewer/Generic/Model/ModelEdgesCard.vue diff --git a/app/components/Viewer/Generic/Model/ComponentEdgesOptions.vue b/app/components/Viewer/Generic/Model/ComponentEdgesOptions.vue new file mode 100644 index 000000000..8ef8d800d --- /dev/null +++ b/app/components/Viewer/Generic/Model/ComponentEdgesOptions.vue @@ -0,0 +1,223 @@ + + + diff --git a/app/components/Viewer/Generic/Model/EdgesOptions.vue b/app/components/Viewer/Generic/Model/EdgesOptions.vue index 836adbc69..5ed542946 100644 --- a/app/components/Viewer/Generic/Model/EdgesOptions.vue +++ b/app/components/Viewer/Generic/Model/EdgesOptions.vue @@ -2,36 +2,14 @@ import ViewerContextMenuItem, { type ItemProps, } from "@ogw_front/components/Viewer/ContextMenu/ContextMenuItem.vue"; +import ModelEdgesCard from "./ModelEdgesCard.vue"; import SurfaceEdges from "@ogw_front/assets/viewer_svgs/surface_edges.svg"; -import ViewerOptionsVisibilitySwitch from "@ogw_front/components/Viewer/Options/VisibilitySwitch.vue"; -import { useBatchStyle } from "@ogw_front/composables/batch_style"; -import { useDataStyleStore } from "@ogw_front/stores/data_style"; -import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer"; - -const dataStyleStore = useDataStyleStore(); -const hybridViewerStore = useHybridViewerStore(); -const { applyBatchStyle } = useBatchStyle(); interface Props { itemProps: ItemProps & { index: number }; } const { itemProps } = defineProps(); - -const id = computed(() => (itemProps.meta_data.modelId as string | undefined) || itemProps.id); - -const visibility = computed({ - get: () => dataStyleStore.modelEdgesVisibility(id.value), - set: async (newValue) => { - if (newValue === undefined) { - return; - } - await applyBatchStyle(id.value, (targetId: string) => - dataStyleStore.setModelEdgesVisibility(targetId, newValue), - ); - hybridViewerStore.remoteRender(); - }, -}); diff --git a/app/components/Viewer/Generic/Model/ModelEdgesCard.vue b/app/components/Viewer/Generic/Model/ModelEdgesCard.vue new file mode 100644 index 000000000..9b0182f2b --- /dev/null +++ b/app/components/Viewer/Generic/Model/ModelEdgesCard.vue @@ -0,0 +1,159 @@ + + + + + From cd221976b5affa91ee3d57f84e10e4caf146a611 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:54:29 +0000 Subject: [PATCH 5/6] Apply prepare changes --- app/components/Viewer/Generic/Mesh/EdgesOptions.vue | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/components/Viewer/Generic/Mesh/EdgesOptions.vue b/app/components/Viewer/Generic/Mesh/EdgesOptions.vue index dd10021dc..413704044 100644 --- a/app/components/Viewer/Generic/Mesh/EdgesOptions.vue +++ b/app/components/Viewer/Generic/Mesh/EdgesOptions.vue @@ -30,12 +30,7 @@ interface Props { capabilities?: Record; } -const { - itemProps, - btnImage, - tooltip = "Edges options", - capabilities = {}, -} = defineProps(); +const { itemProps, btnImage, tooltip = "Edges options", capabilities = {} } = defineProps(); const id = toRef(() => itemProps.id); From ab90c37711f1d4094895485c3d511a64fdb8ffca Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 22 Oct 2026 09:46:27 +0200 Subject: [PATCH 6/6] oxlint --- .../stores/data_style/model/edges.nuxt.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/integration/stores/data_style/model/edges.nuxt.test.ts b/tests/integration/stores/data_style/model/edges.nuxt.test.ts index 5637d655b..e6f33ff96 100644 --- a/tests/integration/stores/data_style/model/edges.nuxt.test.ts +++ b/tests/integration/stores/data_style/model/edges.nuxt.test.ts @@ -25,6 +25,14 @@ async function sleep(milliseconds: number): Promise { }); } +function firstElement(array: readonly Item[]): Item { + const [element] = array; + if (element === undefined) { + throw new Error("Expected array to contain at least one element"); + } + return element; +} + let id = ""; let projectFolderPath = ""; @@ -66,10 +74,7 @@ describe("model edges", () => { const dataStyleStore = useDataStyleStore(); const viewerStore = useViewerStore(); const dataStore = useDataStore(); - const [surface_id] = await dataStore.getSurfacesGeodeIds(id); - if (surface_id === undefined) { - throw new Error("Expected the model to contain a surface"); - } + const surface_id = firstElement(await dataStore.getSurfacesGeodeIds(id)); await dataStyleStore.setModelSurfacesEdgesWidth(id, [surface_id], COMPONENT_EDGES_WIDTH); const spy = vi.spyOn(viewerStore, "request"); spy.mockClear();