diff --git a/.gitignore b/.gitignore index a9d2865..3d5321a 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,8 @@ yarn.lock vsc-extension-quickstart.md **/__pycache__/ +.mypy_cache/ +.ruff_cache/ # Fichiers temporaires ou backup *.tmp diff --git a/CHANGELOG.md b/CHANGELOG.md index 6279e45..afe3605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ 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.5.1] - 2026-03-16 + +Various fixes and optimizations. + +### Added +- Selecting object names in text editors hides all other objects (hence highlighting selected object) +- Progress bar for mesh loading + +### Fixed +- Text selection now highlights groups again +- Group sorting order now handles alphanumerical sorting + ## [1.5.0] - 2026-03-13 Rewrote the mesh viewer UI with Svelte, and added new viewer features. diff --git a/CITATION.cff b/CITATION.cff index 055d550..76cf02a 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,4 +1,4 @@ -cff-version: 1.5.0 +cff-version: 1.5.1 title: VS Code Aster message: >- If you use this software, please cite it using the diff --git a/README.md b/README.md index 2f37a93..58fc2be 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Simvia Logo

- Version + Version License

diff --git a/ROADMAP.md b/ROADMAP.md index 1c55674..44f5abe 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.5.0) +## Current Capabilities (v1.5.1) - `.export` file generator - 3D mesh viewer diff --git a/package-lock.json b/package-lock.json index e69fa47..618b7dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vs-code-aster", - "version": "1.5.0", + "version": "1.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vs-code-aster", - "version": "1.5.0", + "version": "1.5.1", "license": "GPL-3.0", "dependencies": { "@tailwindcss/cli": "^4.1.17", diff --git a/package.json b/package.json index c9b05a0..c998a2a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vs-code-aster", "displayName": "VS Code Aster", - "version": "1.5.0", + "version": "1.5.1", "description": "VS Code extension for code_aster", "publisher": "simvia", "license": "GPL-3.0", diff --git a/src/WebviewVisu.ts b/src/WebviewVisu.ts index 5ca0789..46a32a0 100644 --- a/src/WebviewVisu.ts +++ b/src/WebviewVisu.ts @@ -9,6 +9,7 @@ export class WebviewVisu implements vscode.Disposable { public readonly panel: vscode.WebviewPanel; private groups?: string[]; + private objects?: string[]; private selectedGroups: string[]; public get webview(): vscode.Webview { @@ -120,6 +121,7 @@ export class WebviewVisu implements vscode.Disposable { let groupList = e.groupList; console.log('Group list : ', groupList); this.groups = groupList; + this.objects = e.objectList; this.panel.webview.postMessage({ type: 'addGroupButtons', body: { groupList }, @@ -144,6 +146,11 @@ export class WebviewVisu implements vscode.Disposable { * Groups present in the text will be displayed; others will be hidden. * @param text The text to check for group names. */ + private makeWholeTokenRegex(name: string): RegExp { + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(`(? { - const shortName = groupName.includes('::') ? groupName.split('::')[2]! : groupName; - const regex = new RegExp(`\\b${shortName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`); - return regex.test(text); + const shortName = groupName.includes('::') ? groupName.split('::')[1]! : groupName; + return this.makeWholeTokenRegex(shortName).test(text); }) || []; // Hide groups that are no longer in the text this.selectedGroups = this.selectedGroups.filter((oldGroup) => { if (!readGroups.includes(oldGroup)) { - console.log('Hide group:', oldGroup); this.panel.webview.postMessage({ type: 'displayGroup', body: { group: oldGroup, visible: false }, @@ -174,7 +179,6 @@ export class WebviewVisu implements vscode.Disposable { // Show groups that are new to the text readGroups.forEach((group) => { if (!this.selectedGroups.includes(group)) { - console.log('Display group:', group); this.panel.webview.postMessage({ type: 'displayGroup', body: { group, visible: true }, @@ -182,6 +186,20 @@ export class WebviewVisu implements vscode.Disposable { this.selectedGroups.push(group); } }); + + // Parse the text to find object names + // Object keys are like "all_mesh.obj"; match against the stem (no "all_" prefix, no extension) + const selectedObjects = + this.objects?.filter((objectKey) => { + const withoutPrefix = objectKey.startsWith('all_') ? objectKey.slice(4) : objectKey; + const shortName = withoutPrefix.replace(/\.[^.]+$/, ''); + return this.makeWholeTokenRegex(shortName).test(text); + }) || []; + + this.panel.webview.postMessage({ + type: 'showOnlyObjects', + body: { objects: selectedObjects }, + }); } /** @@ -206,11 +224,14 @@ export class WebviewVisu implements vscode.Disposable { const htmlDir = path.dirname(htmlFilePath); // Replace relative paths (href/src/img) with valid URIs for the webview - html = html.replace(/( { - const resourceFullPath = path.join(htmlDir, p2); - const uri = panel.webview.asWebviewUri(vscode.Uri.file(resourceFullPath)); - return `${p1}${uri.toString()}"`; - }); + html = html.replace( + /( { + const resourceFullPath = path.join(htmlDir, p2); + const uri = panel.webview.asWebviewUri(vscode.Uri.file(resourceFullPath)); + return `${p1}${uri.toString()}"`; + } + ); html = html.replace(/\${webview.cspSource}/g, panel.webview.cspSource); diff --git a/webviews/viewer/src/components/LoadingScreen.svelte b/webviews/viewer/src/components/LoadingScreen.svelte deleted file mode 100644 index 0111f3d..0000000 --- a/webviews/viewer/src/components/LoadingScreen.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - -
-
- Loading meshes... -
diff --git a/webviews/viewer/src/components/ZoomWidget.svelte b/webviews/viewer/src/components/ZoomWidget.svelte deleted file mode 100644 index 620ac98..0000000 --- a/webviews/viewer/src/components/ZoomWidget.svelte +++ /dev/null @@ -1,62 +0,0 @@ - - -
-
- - {zoomText} -
- {#if !$isAtDefaultZoom} - - {/if} -
diff --git a/webviews/viewer/src/components/App.svelte b/webviews/viewer/src/components/layout/App.svelte similarity index 77% rename from webviews/viewer/src/components/App.svelte rename to webviews/viewer/src/components/layout/App.svelte index 90a1298..e689b8d 100644 --- a/webviews/viewer/src/components/App.svelte +++ b/webviews/viewer/src/components/layout/App.svelte @@ -1,12 +1,12 @@ + +
+
+
+
+ {$loadingMessage || 'Loading...'} +
diff --git a/webviews/viewer/src/components/Sidebar.svelte b/webviews/viewer/src/components/layout/Sidebar.svelte similarity index 72% rename from webviews/viewer/src/components/Sidebar.svelte rename to webviews/viewer/src/components/layout/Sidebar.svelte index e61cc07..a07090e 100644 --- a/webviews/viewer/src/components/Sidebar.svelte +++ b/webviews/viewer/src/components/layout/Sidebar.svelte @@ -1,10 +1,10 @@ + + (open = false)} /> + +
+ {@render children()} +
+ +{#if open} +
e.stopPropagation()} + onkeydown={(e) => e.stopPropagation()} + role="menu" + tabindex="-1" + > + {#each options as { value: val, label }} + {@const isSelected = val === value} + + {/each} +
+{/if} diff --git a/webviews/viewer/src/components/ui/Toggle.svelte b/webviews/viewer/src/components/ui/Toggle.svelte new file mode 100644 index 0000000..97560bf --- /dev/null +++ b/webviews/viewer/src/components/ui/Toggle.svelte @@ -0,0 +1,42 @@ + + + diff --git a/webviews/viewer/src/components/AxisButtons.svelte b/webviews/viewer/src/components/viewer/AxisButtons.svelte similarity index 92% rename from webviews/viewer/src/components/AxisButtons.svelte rename to webviews/viewer/src/components/viewer/AxisButtons.svelte index 38447ec..f9629f0 100644 --- a/webviews/viewer/src/components/AxisButtons.svelte +++ b/webviews/viewer/src/components/viewer/AxisButtons.svelte @@ -1,5 +1,5 @@
diff --git a/webviews/viewer/src/components/viewer/ZoomWidget.svelte b/webviews/viewer/src/components/viewer/ZoomWidget.svelte new file mode 100644 index 0000000..0206a30 --- /dev/null +++ b/webviews/viewer/src/components/viewer/ZoomWidget.svelte @@ -0,0 +1,55 @@ + + +
+ CameraManager.Instance.setZoom(parseFloat(v))} + align="right" + > +
+ + {zoomText} +
+
+ {#if !$isAtDefaultZoom} + + {/if} +
diff --git a/webviews/viewer/src/lib/Controller.ts b/webviews/viewer/src/lib/Controller.ts index fe7c124..22daf27 100644 --- a/webviews/viewer/src/lib/Controller.ts +++ b/webviews/viewer/src/lib/Controller.ts @@ -2,7 +2,7 @@ import { VtkApp } from './core/VtkApp'; import { CreateGroups } from './data/CreateGroups'; import { VisibilityManager } from './commands/VisibilityManager'; import { CameraManager } from './interaction/CameraManager'; -import { groupHierarchy as groupHierarchyStore } from './state'; +import { groupHierarchy as groupHierarchyStore, loadingProgress, loadingMessage } from './state'; import type { Group } from './data/Group'; export class Controller { @@ -33,20 +33,41 @@ export class Controller { return this._vsCodeApi; } - loadFiles(fileContexts: string[], fileNames: string[]): void { + async loadFiles(fileContexts: string[], fileNames: string[]): Promise { + if (this._groups) { + return; + } + loadingProgress.set(0); + loadingMessage.set(''); const lfr = new CreateGroups(fileContexts, fileNames); - lfr.do(); + await lfr.do( + (progress) => loadingProgress.set(progress), + (message) => loadingMessage.set(message) + ); this._vsCodeApi.postMessage({ type: 'groups', groupList: this.getGroupNames(), + objectList: this.getObjectNames(), }); } saveGroups(groups: Record, groupHierarchy: Record): void { this._groups = groups; - this._groupHierarchy = groupHierarchy; - groupHierarchyStore.set(groupHierarchy); + const naturalSort = (a: string, b: string) => + a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }); + + const sortedHierarchy: Record = {}; + for (const key of Object.keys(groupHierarchy).sort(naturalSort)) { + sortedHierarchy[key] = { + ...groupHierarchy[key], + faces: [...groupHierarchy[key].faces].sort(naturalSort), + nodes: [...groupHierarchy[key].nodes].sort(naturalSort), + }; + } + + this._groupHierarchy = sortedHierarchy; + groupHierarchyStore.set(sortedHierarchy); this.initManagers(); @@ -75,4 +96,11 @@ export class Controller { } return Object.keys(this._groups).filter((key) => key.includes('::')); } + + getObjectNames(): string[] { + if (!this._groupHierarchy) { + return []; + } + return Object.keys(this._groupHierarchy); + } } diff --git a/webviews/viewer/src/lib/commands/VisibilityManager.ts b/webviews/viewer/src/lib/commands/VisibilityManager.ts index afb7f3d..39c0cb9 100644 --- a/webviews/viewer/src/lib/commands/VisibilityManager.ts +++ b/webviews/viewer/src/lib/commands/VisibilityManager.ts @@ -156,6 +156,19 @@ export class VisibilityManager { VtkApp.Instance.getRenderWindow().render(); } + showOnlyObjects(objects: string[]): void { + const objectSet = new Set(objects); + for (const key in this.hiddenObjects) { + const shouldBeVisible = objects.length === 0 || objectSet.has(key); + const isCurrentlyHidden = this.hiddenObjects[key]; + if (shouldBeVisible && isCurrentlyHidden) { + this.toggleObjectVisibility(key); + } else if (!shouldBeVisible && !isCurrentlyHidden) { + this.toggleObjectVisibility(key); + } + } + } + hideAllOthers(object: string): void { for (const key in this.hiddenObjects) { if (key === object) continue; diff --git a/webviews/viewer/src/lib/data/CreateGroups.ts b/webviews/viewer/src/lib/data/CreateGroups.ts index 9b51c7c..a30e108 100644 --- a/webviews/viewer/src/lib/data/CreateGroups.ts +++ b/webviews/viewer/src/lib/data/CreateGroups.ts @@ -16,8 +16,16 @@ export class CreateGroups { this.fileNames = fileNames; } - do(): void { - const result = ObjLoader.loadFiles(this.fileContexts, this.fileNames); + async do( + onProgress: (progress: number) => void, + onMessage: (message: string) => void + ): Promise { + const result = await ObjLoader.loadFiles( + this.fileContexts, + this.fileNames, + onProgress, + onMessage + ); const post = (text: string) => { Controller.Instance.getVSCodeAPI().postMessage({ type: 'debugPanel', text }); }; @@ -40,7 +48,12 @@ export class CreateGroups { const faceActorCreator = new FaceActorCreator(vertices, cells, cellIndexToGroup); const nodeActorCreator = new NodeActorCreator(vertices, nodes, nodeIndexToGroup); - for (const fileGroup in groupHierarchy) { + const groupKeys = Object.keys(groupHierarchy); + const yield_ = () => new Promise((r) => setTimeout(r, 0)); + + onMessage('Building scene...'); + for (let gi = 0; gi < groupKeys.length; gi++) { + const fileGroup = groupKeys[gi]; const groupId = faceGroups.indexOf(fileGroup); const _oc = GlobalSettings.Instance.objectColors; @@ -104,6 +117,9 @@ export class CreateGroups { ); this.groups[`${fileGroup}::${nodeGroup}::node`] = subGroup; } + + onProgress(0.9 + ((gi + 1) / groupKeys.length) * 0.1); + await yield_(); } VtkApp.Instance.getRenderer().resetCamera(); diff --git a/webviews/viewer/src/lib/data/ObjLoader.ts b/webviews/viewer/src/lib/data/ObjLoader.ts index ee3d42f..866fb4b 100644 --- a/webviews/viewer/src/lib/data/ObjLoader.ts +++ b/webviews/viewer/src/lib/data/ObjLoader.ts @@ -11,8 +11,15 @@ export interface ObjLoaderResult { groupHierarchy: Record; } +const YIELD_EVERY_LINES = 5_000; + export class ObjLoader { - static loadFiles(fileContexts: string[], fileNames: string[]): ObjLoaderResult { + static async loadFiles( + fileContexts: string[], + fileNames: string[], + onProgress: (progress: number) => void, + onMessage: (message: string) => void + ): Promise { const vertices: { x: number; y: number; z: number }[] = []; const cells: number[][] = []; const cellIndexToGroup: number[] = []; @@ -26,6 +33,8 @@ export class ObjLoader { let groupId = -1; let nodeGroupId = -1; + const yield_ = () => new Promise((r) => setTimeout(r, 0)); + for (let i = 0; i < fileContexts.length; i++) { try { groupId++; @@ -35,9 +44,17 @@ export class ObjLoader { faceGroups.push(skinName); nbVertices = vertices.length; + onMessage(`Parsing ${fileNames[i]}...`); const lines = fileContexts[i].split('\n').map((l) => l.replace('\r', '')); + const totalLines = lines.length; + + for (let lineIdx = 0; lineIdx < totalLines; lineIdx++) { + if (lineIdx % YIELD_EVERY_LINES === 0 && lineIdx > 0) { + const fileProgress = (i + lineIdx / totalLines) / fileContexts.length; + onProgress(fileProgress * 0.9); + await yield_(); + } - for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { const line = lines[lineIdx]; const ss = line.split(' ').filter((p) => p.length !== 0); if (ss.length === 0) { @@ -84,6 +101,9 @@ export class ObjLoader { } } } + + onProgress(((i + 1) / fileContexts.length) * 0.9); + await yield_(); } catch (fileError: any) { Controller.Instance.getVSCodeAPI().postMessage({ type: 'debugPanel', diff --git a/webviews/viewer/src/lib/state.ts b/webviews/viewer/src/lib/state.ts index f1cc5f5..ce95f2b 100644 --- a/webviews/viewer/src/lib/state.ts +++ b/webviews/viewer/src/lib/state.ts @@ -1,4 +1,6 @@ import { writable } from 'svelte/store'; +import { tweened } from 'svelte/motion'; +import { cubicOut } from 'svelte/easing'; export type GroupHierarchy = Record; export type HighlightedGroups = Map; @@ -29,3 +31,6 @@ export const settings = writable({ // Map> — groups NOT shown in sidebar (hidden) export const sidebarHiddenGroups = writable>>(new Map()); + +export const loadingProgress = tweened(0, { duration: 300, easing: cubicOut }); +export const loadingMessage = writable(''); diff --git a/webviews/viewer/src/lib/ui/CustomDropdown.ts b/webviews/viewer/src/lib/ui/CustomDropdown.ts deleted file mode 100644 index 5134a2c..0000000 --- a/webviews/viewer/src/lib/ui/CustomDropdown.ts +++ /dev/null @@ -1,113 +0,0 @@ -export interface DropdownOption { - value: string; - label: string; -} - -export class CustomDropdown { - private _trigger: HTMLElement; - private _options: DropdownOption[]; - private _onSelect: (value: string) => void; - private _getValue: (() => string | null) | null; - private _align: 'left' | 'right'; - private _panel: HTMLElement | null = null; - - constructor( - trigger: HTMLElement, - options: DropdownOption[], - onSelect: (value: string) => void, - getValue: (() => string | null) | null = null, - opts: { align?: 'left' | 'right' } = {} - ) { - this._trigger = trigger; - this._options = options; - this._onSelect = onSelect; - this._getValue = getValue; - this._align = opts.align ?? 'left'; - - trigger.addEventListener('click', (e) => { - e.stopPropagation(); - this._panel ? this.close() : this._open(); - }); - document.addEventListener('click', () => this.close()); - } - - private _open(): void { - const currentValue = this._getValue?.(); - const showCheckmarks = this._getValue != null; - - const panel = document.createElement('div'); - panel.style.cssText = ` - position: fixed; - z-index: 9999; - background: var(--ui-popup-bg); - border: 1px solid var(--ui-border); - border-radius: 4px; - box-shadow: 0 4px 16px rgba(0,0,0,0.25); - padding: 3px 0; - overflow: hidden; - `; - panel.addEventListener('click', (e) => e.stopPropagation()); - - for (const { value, label } of this._options) { - const isSelected = value === currentValue; - const item = document.createElement('div'); - item.style.cssText = ` - display: flex; - align-items: center; - gap: 8px; - padding: 5px 10px; - font-size: 0.75rem; - cursor: pointer; - color: var(--ui-fg); - white-space: nowrap; - ${this._align === 'right' ? 'justify-content: flex-end;' : ''} - ${isSelected ? 'font-weight: 600;' : ''} - `; - - if (showCheckmarks) { - const check = document.createElement('span'); - check.textContent = '✓'; - check.style.cssText = `font-size: 0.6rem; flex-shrink: 0; width: 10px; opacity: ${isSelected ? 1 : 0};`; - item.appendChild(check); - } - - const labelEl = document.createElement('span'); - labelEl.textContent = label; - item.appendChild(labelEl); - - item.addEventListener('mouseenter', () => { - item.style.background = 'var(--ui-element-bg-hover)'; - }); - item.addEventListener('mouseleave', () => { - item.style.background = ''; - }); - item.addEventListener('click', () => { - this._onSelect(value); - this.close(); - }); - - panel.appendChild(item); - } - - document.body.appendChild(panel); - - const rect = this._trigger.getBoundingClientRect(); - const panelW = Math.max(panel.offsetWidth, rect.width); - const panelH = panel.offsetHeight; - panel.style.minWidth = `${panelW}px`; - - let left = rect.left + rect.width / 2 - panelW / 2; - left = Math.max(4, Math.min(left, window.innerWidth - panelW - 4)); - panel.style.left = `${left}px`; - - const openUp = window.innerHeight - rect.bottom < panelH + 8; - panel.style.top = openUp ? `${rect.top - panelH - 4}px` : `${rect.bottom + 4}px`; - - this._panel = panel; - } - - close(): void { - this._panel?.remove(); - this._panel = null; - } -} diff --git a/webviews/viewer/src/main.ts b/webviews/viewer/src/main.ts index 08f6f9e..b7fd10f 100644 --- a/webviews/viewer/src/main.ts +++ b/webviews/viewer/src/main.ts @@ -1,5 +1,5 @@ import { mount } from 'svelte'; -import App from './components/App.svelte'; +import App from './components/layout/App.svelte'; import { Controller } from './lib/Controller'; import { VisibilityManager } from './lib/commands/VisibilityManager'; import { CameraManager } from './lib/interaction/CameraManager'; @@ -57,6 +57,10 @@ window.addEventListener('message', async (e) => { case 'displayGroup': VisibilityManager.Instance.setVisibility(body.group, body.visible); break; + + case 'showOnlyObjects': + VisibilityManager.Instance.showOnlyObjects(body.objects); + break; } });