From 781e36ba902aa8b71f71c11af57e9310ce31862c Mon Sep 17 00:00:00 2001 From: Ulysse Bouchet Date: Thu, 23 Apr 2026 13:41:13 +0200 Subject: [PATCH 1/3] feat: render standalone 1D edges by default with outlined style Detect edges not shared with any face and render them as outlined colored lines attached to the parent file group. Line style adapts to parent opacity, wireframe mode, and face-edge visibility rules. Switch the file-group skin detection from a loose `includes('all_')` substring check to an explicit isObject flag so user-defined face groups like `all_plates` are no longer treated as object actors. --- .../src/lib/commands/VisibilityManager.ts | 20 ++++--- webviews/viewer/src/lib/data/CreateGroups.ts | 36 +++++++++++- webviews/viewer/src/lib/data/Group.ts | 49 ++++++++++++++++- .../src/lib/data/create/EdgeActorCreator.ts | 55 +++++++++++++++++++ .../src/lib/data/create/FaceActorCreator.ts | 13 +++-- .../src/lib/interaction/CameraManager.ts | 1 + 6 files changed, 157 insertions(+), 17 deletions(-) diff --git a/webviews/viewer/src/lib/commands/VisibilityManager.ts b/webviews/viewer/src/lib/commands/VisibilityManager.ts index f3dceb4..dc0d5ea 100644 --- a/webviews/viewer/src/lib/commands/VisibilityManager.ts +++ b/webviews/viewer/src/lib/commands/VisibilityManager.ts @@ -118,16 +118,16 @@ export class VisibilityManager { const fileGroup = this.groups[object]; if (fileGroup) { if (nowVisible) { - fileGroup.actor.setVisibility(true); + fileGroup.setVisibility(true); const opacity = this.visibleGroupsByObject[object] > 0 ? GlobalSettings.Instance.groupTransparency : 1.0; fileGroup.setOpacity(opacity); } else { const hiddenOpacity = GlobalSettings.Instance.hiddenObjectOpacity; if (hiddenOpacity === 0) { - fileGroup.actor.setVisibility(false); + fileGroup.setVisibility(false); } else { - fileGroup.actor.setVisibility(true); + fileGroup.setVisibility(true); fileGroup.setOpacity(hiddenOpacity); } } @@ -165,8 +165,9 @@ export class VisibilityManager { applyEdgeGroupThickness(): void { const thickness = GlobalSettings.Instance.edgeGroupThickness; for (const group of Object.values(this.groups)) { - if (group.kind !== 'edge') continue; - group.actor.getProperty().setLineWidth(thickness); + if (group.kind === 'edge') { + group.actor.getProperty().setLineWidth(thickness); + } } VtkApp.Instance.getRenderWindow().render(); } @@ -174,8 +175,9 @@ export class VisibilityManager { applyEdgeGroupDepthOffset(): void { const enabled = GlobalSettings.Instance.edgeGroupDepthOffset; for (const group of Object.values(this.groups)) { - if (group.kind !== 'edge') continue; - EdgeActorCreator.applyDepthOffset(group.actor.getMapper(), enabled); + if (group.kind === 'edge') { + EdgeActorCreator.applyDepthOffset(group.actor.getMapper(), enabled); + } } VtkApp.Instance.getRenderWindow().render(); } @@ -213,9 +215,9 @@ export class VisibilityManager { const fileGroup = this.groups[object]; if (!fileGroup) continue; if (hiddenOpacity === 0) { - fileGroup.actor.setVisibility(false); + fileGroup.setVisibility(false); } else { - fileGroup.actor.setVisibility(true); + fileGroup.setVisibility(true); fileGroup.setOpacity(hiddenOpacity); } } diff --git a/webviews/viewer/src/lib/data/CreateGroups.ts b/webviews/viewer/src/lib/data/CreateGroups.ts index 3c823bd..d4dc0b4 100644 --- a/webviews/viewer/src/lib/data/CreateGroups.ts +++ b/webviews/viewer/src/lib/data/CreateGroups.ts @@ -53,6 +53,31 @@ export class CreateGroups { const nodeActorCreator = new NodeActorCreator(vertices, nodes, nodeIndexToGroup); const edgeActorCreator = new EdgeActorCreator(vertices, edges, edgeIndexToGroup); + const edgeKey = (a: number, b: number) => (a < b ? `${a}-${b}` : `${b}-${a}`); + const faceEdgeSet = new Set(); + for (const cell of cells) { + for (let i = 0; i < cell.length; i++) { + faceEdgeSet.add(edgeKey(cell[i], cell[(i + 1) % cell.length])); + } + } + const standaloneByFile: Record = {}; + for (let i = 0; i < edges.length; i++) { + const e = edges[i]; + let isStandalone = false; + for (let j = 0; j + 1 < e.length; j++) { + if (!faceEdgeSet.has(edgeKey(e[j], e[j + 1]))) { + isStandalone = true; + break; + } + } + if (!isStandalone) continue; + const egId = edgeIndexToGroup[i]; + if (egId < 0) continue; + const fileGroup = edgeGroups[egId]?.split('::')[0]; + if (!fileGroup) continue; + (standaloneByFile[fileGroup] ||= []).push(i); + } + const groupKeys = Object.keys(groupHierarchy); const yield_ = () => new Promise((r) => setTimeout(r, 0)); @@ -70,7 +95,7 @@ export class CreateGroups { colorIndex: fileColorIndex, isObjectActor: fileIsObj, cellCount: fileCellCount, - } = faceActorCreator.create(fileGroup, groupId); + } = faceActorCreator.create(fileGroup, groupId, true); const groupInstance = new Group( actor, @@ -84,6 +109,15 @@ export class CreateGroups { ); this.groups[fileGroup] = groupInstance; + const standaloneIdx = standaloneByFile[fileGroup]; + if (standaloneIdx && standaloneIdx.length > 0) { + const result = edgeActorCreator.createStandalone(standaloneIdx, objColor); + if (result) { + groupInstance.standaloneEdgesActor = result.actor; + groupInstance.standaloneEdgesContourActor = result.contourActor; + } + } + const size = this.computeSize(actor); for (const volumeGroup of groupHierarchy[fileGroup].volumes) { diff --git a/webviews/viewer/src/lib/data/Group.ts b/webviews/viewer/src/lib/data/Group.ts index 79292aa..d0c1cac 100644 --- a/webviews/viewer/src/lib/data/Group.ts +++ b/webviews/viewer/src/lib/data/Group.ts @@ -11,7 +11,14 @@ export class Group { colorIndex: number | null; isObjectActor: boolean; cellCount: number | null; + standaloneEdgesActor: any = null; + standaloneEdgesContourActor: any = null; private _edgeT?: number; + private _visible = true; + private _opacity = 1; + private _wireframe = false; + private _edgeVisible = true; + private _edgeFade = 1; constructor( actor: any, @@ -44,6 +51,9 @@ export class Group { : GlobalSettings.Instance.meshGroupColors; const color = colors[this.colorIndex % colors.length]; this.actor.getProperty().setColor(color); + if (this.standaloneEdgesActor) { + this.standaloneEdgesActor.getProperty().setColor(color[0], color[1], color[2]); + } this._applyEdgeColor(); } @@ -54,11 +64,17 @@ export class Group { if (mode === 'hide') { prop.setEdgeVisibility(false); + this._edgeVisible = false; + this._edgeFade = 0; + this._updateStandaloneEdges(); return; } if (mode === 'show') { prop.setEdgeVisibility(true); this._applyFlatEdgeColor(prop); + this._edgeVisible = true; + this._edgeFade = 1; + this._updateStandaloneEdges(); return; } @@ -68,14 +84,21 @@ export class Group { GlobalSettings.Instance.edgeThresholdMultiplier; if (mode === 'threshold') { - prop.setEdgeVisibility(currentDistance < threshold); + const visible = currentDistance < threshold; + prop.setEdgeVisibility(visible); this._applyFlatEdgeColor(prop); + this._edgeVisible = visible; + this._edgeFade = visible ? 1 : 0; + this._updateStandaloneEdges(); return; } prop.setEdgeVisibility(true); this._edgeT = Math.min(1, Math.max(0, threshold / currentDistance)); this._applyEdgeColor(); + this._edgeVisible = true; + this._edgeFade = this._edgeT; + this._updateStandaloneEdges(); } private _applyFlatEdgeColor(prop: any): void { @@ -103,10 +126,34 @@ export class Group { } setVisibility(visible: boolean): void { + this._visible = visible; this.actor.setVisibility(visible); + this._updateStandaloneEdges(); } setOpacity(opacity: number): void { + this._opacity = opacity; this.actor.getProperty().setOpacity(opacity); + this._updateStandaloneEdges(); + } + + setWireframeMode(wireframe: boolean): void { + this._wireframe = wireframe; + this._updateStandaloneEdges(); + } + + private _updateStandaloneEdges(): void { + if (!this.standaloneEdgesActor) return; + const transparent = this._opacity < 1; + const thin = transparent || this._wireframe; + const lineOpacity = transparent ? this._opacity * 0.4 : this._opacity; + this.standaloneEdgesActor.setVisibility(this._visible); + this.standaloneEdgesActor.getProperty().setOpacity(lineOpacity); + this.standaloneEdgesActor.getProperty().setLineWidth(thin ? 1 : 2); + if (this.standaloneEdgesContourActor) { + const contourVisible = this._visible && !thin && this._edgeVisible; + this.standaloneEdgesContourActor.setVisibility(contourVisible); + this.standaloneEdgesContourActor.getProperty().setOpacity(lineOpacity * this._edgeFade); + } } } diff --git a/webviews/viewer/src/lib/data/create/EdgeActorCreator.ts b/webviews/viewer/src/lib/data/create/EdgeActorCreator.ts index 531d2d1..42e3fcb 100644 --- a/webviews/viewer/src/lib/data/create/EdgeActorCreator.ts +++ b/webviews/viewer/src/lib/data/create/EdgeActorCreator.ts @@ -37,6 +37,61 @@ export class EdgeActorCreator { return { actor, colorIndex, cellCount }; } + createStandalone( + edgeIndices: number[], + color: number[] + ): { actor: any; contourActor: any; cellCount: number } | null { + if (edgeIndices.length === 0) return null; + + const pd = vtkPolyData.newInstance(); + const pts = vtkPoints.newInstance(); + const coords = new Float32Array(this.vertices.length * 3); + this.vertices.forEach((v, i) => { + coords[3 * i] = v.x; + coords[3 * i + 1] = v.y; + coords[3 * i + 2] = v.z; + }); + pts.setData(coords, 3); + pd.setPoints(pts); + + const lineArray = vtkCellArray.newInstance({ + values: Uint32Array.from( + edgeIndices.flatMap((i) => { + const e = this.edges[i]; + return [e.length, ...e]; + }) + ), + }); + pd.setLines(lineArray); + + const contourMapper = vtkMapper.newInstance(); + contourMapper.setInputData(pd); + contourMapper.setResolveCoincidentTopologyToPolygonOffset(); + contourMapper.setRelativeCoincidentTopologyLineOffsetParameters(2, 2); + const contourActor = vtkActor.newInstance(); + contourActor.setMapper(contourMapper); + const contourProp = contourActor.getProperty(); + contourProp.setColor(0, 0, 0); + contourProp.setLineWidth(4); + contourProp.setLighting(false); + VtkApp.Instance.getRenderer().addActor(contourActor); + + const mapper = vtkMapper.newInstance(); + mapper.setInputData(pd); + EdgeActorCreator.applyDepthOffset(mapper, false); + + const actor = vtkActor.newInstance(); + actor.setMapper(mapper); + + const prop = actor.getProperty(); + prop.setColor(color[0], color[1], color[2]); + prop.setLineWidth(2); + prop.setLighting(false); + + VtkApp.Instance.getRenderer().addActor(actor); + return { actor, contourActor, cellCount: edgeIndices.length }; + } + static applyDepthOffset(mapper: any, enabled: boolean): void { if (enabled) { mapper.setResolveCoincidentTopologyToPolygonOffset(); diff --git a/webviews/viewer/src/lib/data/create/FaceActorCreator.ts b/webviews/viewer/src/lib/data/create/FaceActorCreator.ts index c092d68..f36f4a5 100644 --- a/webviews/viewer/src/lib/data/create/FaceActorCreator.ts +++ b/webviews/viewer/src/lib/data/create/FaceActorCreator.ts @@ -22,8 +22,9 @@ export class FaceActorCreator { } create( - groupName: string, - groupId: number + _groupName: string, + groupId: number, + isObject = false ): { actor: any; colorIndex: number; isObjectActor: boolean; cellCount: number } { const { polyData, cellCount } = this.prepare(groupId); @@ -31,13 +32,13 @@ export class FaceActorCreator { const mapper = vtkMapper.newInstance(); mapper.setInputData(polyData); - if (!groupName.includes('all_')) { + if (!isObject) { mapper.setResolveCoincidentTopologyToPolygonOffset(); mapper.setRelativeCoincidentTopologyPolygonOffsetParameters(-2, -2); } actor.setMapper(mapper); - const { colorIndex, isObjectActor } = this.setProperty(actor, groupName, cellCount); + const { colorIndex, isObjectActor } = this.setProperty(actor, isObject, cellCount); VtkApp.Instance.getRenderer().addActor(actor); return { actor, colorIndex, isObjectActor, cellCount }; @@ -78,14 +79,14 @@ export class FaceActorCreator { private setProperty( actor: any, - groupName: string, + isObject: boolean, _cellCount: number ): { colorIndex: number; isObjectActor: boolean } { const prop = actor.getProperty(); let colorIndex: number; let isObjectActor: boolean; - if (groupName.includes('all_')) { + if (isObject) { isObjectActor = true; colorIndex = GlobalSettings.Instance.objIndex; prop.setColor(GlobalSettings.Instance.getColorForObject()); diff --git a/webviews/viewer/src/lib/interaction/CameraManager.ts b/webviews/viewer/src/lib/interaction/CameraManager.ts index f26a519..c4c7feb 100644 --- a/webviews/viewer/src/lib/interaction/CameraManager.ts +++ b/webviews/viewer/src/lib/interaction/CameraManager.ts @@ -215,6 +215,7 @@ export class CameraManager { const rep = wireframe ? 1 : 2; for (const group of Object.values(this.faceGroups)) { group.actor.getProperty().setRepresentation(rep); + group.setWireframeMode(wireframe); } VtkApp.Instance.getRenderWindow().render(); } From 10baa0dc529577898763471c30dd52a24cabc210 Mon Sep 17 00:00:00 2001 From: Ulysse Bouchet Date: Thu, 23 Apr 2026 13:41:32 +0200 Subject: [PATCH 2/3] fix: disable face specular to avoid view-angle color shift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Specular highlights were tinting face colors toward white at camera-facing angles (e.g. blue → turquoise). Flat shading keeps the assigned object/group color stable across viewpoints. --- webviews/viewer/src/lib/data/create/FaceActorCreator.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/webviews/viewer/src/lib/data/create/FaceActorCreator.ts b/webviews/viewer/src/lib/data/create/FaceActorCreator.ts index f36f4a5..8dd1462 100644 --- a/webviews/viewer/src/lib/data/create/FaceActorCreator.ts +++ b/webviews/viewer/src/lib/data/create/FaceActorCreator.ts @@ -104,8 +104,7 @@ export class FaceActorCreator { prop.setLineWidth(0.3); prop.setInterpolationToPhong(); prop.setAmbient(GlobalSettings.Instance.ambientIntensity); - prop.setSpecular(GlobalSettings.Instance.specular); - prop.setSpecularPower(GlobalSettings.Instance.specularPower); + prop.setSpecular(0); return { colorIndex, isObjectActor }; } From 132da1a0c79a60a8861a75828f4436b224d8152a Mon Sep 17 00:00:00 2001 From: Ulysse Bouchet Date: Thu, 23 Apr 2026 14:48:43 +0200 Subject: [PATCH 3/3] [1.9.2] Bump version to 1.9.2 --- CHANGELOG.md | 11 +++++++++++ CITATION.cff | 2 +- README.md | 2 +- ROADMAP.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb981c1..47b0896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to the **VS Code Aster** extension will be documented in thi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.9.2] - 2026-04-23 + +Better rendering for 1D meshes and a flatter, more readable face shading. + +### Added +- **Standalone 1D edges** (edges not shared with any face, e.g. beam elements) now render by default in the object color with a thin black contour. Line style adapts to parent opacity, wireframe mode, and the face-edge visibility rules (hide / show / threshold / gradual). + +### Fixed +- User-defined face groups whose name contains `all_` (e.g. `all_plates`) are no longer mistakenly treated as file-level object actors. +- Face specular highlight removed, so colors no longer shift toward white (e.g. blue → turquoise) at camera-facing angles. + ## [1.9.1] - 2026-04-23 New viewer toolbar actions (auto-rotate, video recording), a reorganized settings popup with a dedicated Toolbar tab, and a round of `.export` editor fixes. diff --git a/CITATION.cff b/CITATION.cff index 72abd27..db09f96 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,4 +1,4 @@ -cff-version: 1.9.1 +cff-version: 1.9.2 title: VS Code Aster message: >- If you use this software, please cite it using the diff --git a/README.md b/README.md index 1a919c7..18ca399 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Simvia Logo

- Version + Version License CI Status GitHub issues diff --git a/ROADMAP.md b/ROADMAP.md index 511e309..a66e2f0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ The extension aims to reduce friction between modeling, validation, execution, and analysis by bringing **code_aster** native workflows into the editor. -## Current Capabilities (v1.9.1) +## Current Capabilities (v1.9.2) - `.export` file generator - 3D mesh viewer diff --git a/package-lock.json b/package-lock.json index aa905ef..20634c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vs-code-aster", - "version": "1.9.1", + "version": "1.9.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vs-code-aster", - "version": "1.9.1", + "version": "1.9.2", "license": "GPL-3.0", "dependencies": { "@kitware/vtk.js": "^35.10.0", diff --git a/package.json b/package.json index bb48e94..8a429cd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vs-code-aster", "displayName": "VS Code Aster", - "version": "1.9.1", + "version": "1.9.2", "description": "VS Code extension for code_aster", "publisher": "simvia", "license": "GPL-3.0",