From 827e2b78aeb54b9b54ce14815edab68aa015b262 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Sat, 31 Oct 2026 20:42:51 +0100 Subject: [PATCH 1/7] Extract legend UI from IdfViewer --- lib/modules/components/idf-viewer.js | 96 +++++++--------------------- 1 file changed, 24 insertions(+), 72 deletions(-) diff --git a/lib/modules/components/idf-viewer.js b/lib/modules/components/idf-viewer.js index 7c96b75..5bb154f 100644 --- a/lib/modules/components/idf-viewer.js +++ b/lib/modules/components/idf-viewer.js @@ -133,13 +133,6 @@ const TEMPLATE = /* html */ ` .toolbar button:hover, .toolbar .file-btn:hover { background: #f3f5f8; } .toolbar .file-btn { font-weight: 600; } .toolbar .file-btn[hidden] { display: none; } - .legend { top: 8px; right: 8px; max-height: calc(100% - 16px); overflow: auto; min-width: 140px; } - .legend h4 { margin: 0 0 4px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; color: #57606a; } - .legend h4 + h4 { margin-top: 8px; } - .legend label { display: flex; align-items: center; gap: 6px; cursor: pointer; padding: 1px 0; } - .legend .sw { width: 12px; height: 12px; border-radius: 3px; border: 1px solid rgba(0,0,0,0.25); flex: none; } - .legend .key { display: flex; align-items: center; gap: 6px; padding: 1px 0; color: #444; } - .legend .muted { color: #8b949e; } .tooltip { position: absolute; pointer-events: none; z-index: 2; background: rgba(31, 35, 40, 0.92); color: #fff; padding: 6px 8px; border-radius: 5px; @@ -157,7 +150,7 @@ const TEMPLATE = /* html */ ` position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; text-align: center; color: #57606a; pointer-events: none; font-size: 15px; } - .empty[hidden], .tooltip[hidden], .status[hidden], .legend[hidden], .toolbar[hidden] { display: none; } + .empty[hidden], .tooltip[hidden], .status[hidden], .toolbar[hidden] { display: none; } :host(.dragover) { outline: 3px dashed #4e79a7; outline-offset: -3px; }
@@ -173,7 +166,6 @@ const TEMPLATE = /* html */ ` -
Déposez un fichier .idf ici
ou utilisez « Ouvrir un IDF… »
@@ -216,7 +208,6 @@ export class IdfViewer extends HTMLElement { interior: this.shadowRoot.querySelector('input[name=interior]'), windows: this.shadowRoot.querySelector('input[name=windows]'), fit: this.shadowRoot.querySelector('button[name=fit]'), - legend: this.shadowRoot.querySelector('.legend'), tooltip: this.shadowRoot.querySelector('.tooltip'), empty: this.shadowRoot.querySelector('.empty'), status: this.shadowRoot.querySelector('.status'), @@ -259,7 +250,7 @@ export class IdfViewer extends HTMLElement { this._els.toolbar.hidden = this.hasAttribute('no-toolbar'); this._els.fileBtn.hidden = this.hasAttribute('src'); this._grid.visible = !this.hasAttribute('no-grid'); - this._refreshLegend(); + this._emitLegendData(); } // ----- API publique ------------------------------------------------------ @@ -319,7 +310,7 @@ export class IdfViewer extends HTMLElement { this._disposeGroup(); this._model = null; this._els.empty.hidden = false; - this._refreshLegend(); + this._emitLegendData(); } /** Recentre la caméra sur le modèle. */ @@ -377,7 +368,7 @@ export class IdfViewer extends HTMLElement { this._els.mode.value = m; if (this.getAttribute('color-mode') !== m) this.setAttribute('color-mode', m); this._applyColors(); - this._refreshLegend(); + this._emitLegendData(); } get showEdges() { @@ -411,7 +402,7 @@ export class IdfViewer extends HTMLElement { setZoneVisible(zoneName, visible) { this._zoneVisible.set(zoneName, !!visible); this._applyVisibility(); - this._refreshLegend(); + this._emitLegendData(); } /** Accès aux objets three.js pour les usages avancés. */ @@ -583,7 +574,7 @@ export class IdfViewer extends HTMLElement { this._applyColors(); this._applyVisibility(); - this._refreshLegend(); + this._emitLegendData(); this.fitView(); } @@ -692,65 +683,26 @@ export class IdfViewer extends HTMLElement { t.hidden = false; } - _refreshLegend() { - const legend = this._els.legend; - if (!this._model || this.hasAttribute('no-legend')) { - legend.hidden = true; - return; - } - const parts = []; - const hex = (c) => `#${c.toString(16).padStart(6, '0')}`; - - parts.push('

Zones

'); - for (const z of this._model.zones) { - const color = this._colorMode === 'zone' ? hex(this._zoneColors.get(z.name)) : '#c9ced6'; - const checked = this._zoneVisible.get(z.name) !== false ? 'checked' : ''; - const count = this._model.surfaces.filter((s) => s.zone === z.name).length; - parts.push( - `` - ); - } + /** Return legend data without exposing Three.js internals. */ + getLegendData() { + if (!this._model || this.hasAttribute('no-legend')) return null; + const hex = (c) => '#' + c.toString(16).padStart(6, '0'); + const zones = this._model.zones.map((z) => ({ + name: z.name, + count: this._model.surfaces.filter((s) => s.zone === z.name).length, + color: this._colorMode === 'zone' ? hex(this._zoneColors.get(z.name)) : '#c9ced6', + visible: this._zoneVisible.get(z.name) !== false, + })); let keys = []; + if (this._colorMode === 'boundary') keys = [['Extérieur', BOUNDARY_COLORS.outdoors], ['Sol', BOUNDARY_COLORS.ground], ['Surface / zone', BOUNDARY_COLORS.surface], ['Adiabatique', BOUNDARY_COLORS.adiabatic], ['Autre', BOUNDARY_COLORS.other]]; + if (this._colorMode === 'type') keys = [['Mur extérieur', TYPE_COLORS.wall], ['Mur intérieur', TYPE_COLORS.wallInterior], ['Mur enterré', TYPE_COLORS.wallGround], ['Toiture', TYPE_COLORS.roof], ['Plafond', TYPE_COLORS.ceiling], ['Plancher', TYPE_COLORS.floor], ['Plancher sur sol', TYPE_COLORS.floorGround]]; + if (this._model.fenestrations.length) keys.push(['Fenêtre', TYPE_COLORS.window], ['Porte vitrée', TYPE_COLORS.glassdoor], ['Porte', TYPE_COLORS.door]); + if (this._model.shading.length) keys.push(['Masque', TYPE_COLORS.shading]); + return { colorMode: this._colorMode, zones, keys: keys.map(([label, color]) => ({ label, color: hex(color) })) }; + } - switch (this._colorMode) { - case 'boundary': - keys = [ - ['Extérieur', BOUNDARY_COLORS.outdoors], - ['Sol', BOUNDARY_COLORS.ground], - ['Surface / zone', BOUNDARY_COLORS.surface], - ['Adiabatique', BOUNDARY_COLORS.adiabatic], - ['Autre', BOUNDARY_COLORS.other], - ]; - break; - case 'type': - keys = [ - ['Mur extérieur', TYPE_COLORS.wall], - ['Mur intérieur', TYPE_COLORS.wallInterior], - ['Mur enterré', TYPE_COLORS.wallGround], - ['Toiture', TYPE_COLORS.roof], - ['Plafond', TYPE_COLORS.ceiling], - ['Plancher', TYPE_COLORS.floor], - ['Plancher sur sol', TYPE_COLORS.floorGround], - ]; - break; - } - const fenKeys = [['Fenêtre', TYPE_COLORS.window], ['Porte vitrée', TYPE_COLORS.glassdoor], ['Porte', TYPE_COLORS.door]]; - const shadingKeys = this._model.shading.length ? [['Masque', TYPE_COLORS.shading]] : []; - const all = [...keys, ...(this._model.fenestrations.length ? fenKeys : []), ...shadingKeys]; - if (all.length) { - parts.push('

Légende

'); - for (const [label, color] of all) parts.push(`
${label}
`); - } - - legend.innerHTML = parts.join(''); - legend.hidden = false; - legend.querySelectorAll('input[data-zone]').forEach((cb) => { - cb.addEventListener('change', () => { - this._zoneVisible.set(cb.dataset.zone, cb.checked); - this._applyVisibility(); - }); - }); + _emitLegendData() { + this.dispatchEvent(new CustomEvent('legend-data-changed', { detail: { data: this.getLegendData() }, bubbles: true, composed: true })); } _setStatus(message, isError = false) { From 2533b1bd77259a381f72de9887936ee082f59553 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Sat, 31 Oct 2026 20:42:53 +0100 Subject: [PATCH 2/7] Add standalone IdfLegend web component --- lib/modules/components/idf-legend.js | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 lib/modules/components/idf-legend.js diff --git a/lib/modules/components/idf-legend.js b/lib/modules/components/idf-legend.js new file mode 100644 index 0000000..6d95ab9 --- /dev/null +++ b/lib/modules/components/idf-legend.js @@ -0,0 +1,52 @@ +/** Standalone legend for an element. */ +const TEMPLATE = /* html */ ` +`; + +export class IdfLegend extends HTMLElement { + static get observedAttributes() { return ['viewer']; } + constructor() { + super(); this.attachShadow({ mode:'open' }); this.shadowRoot.innerHTML = TEMPLATE; + this._panel = this.shadowRoot.querySelector('.panel'); this._viewer = null; + this._onData = (event) => this._render(event.detail?.data ?? null); + } + connectedCallback() { this._connectViewer(); } + disconnectedCallback() { this._disconnectViewer(); } + attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue && this.isConnected) this._connectViewer(); } + _connectViewer() { + this._disconnectViewer(); + const id = this.getAttribute('viewer'); + this._viewer = id ? this.ownerDocument.getElementById(id) : this.parentElement?.querySelector('idf-viewer'); + if (!this._viewer || this._viewer.localName !== 'idf-viewer') { this._render(null); return; } + this._viewer.addEventListener('legend-data-changed', this._onData); + this._render(this._viewer.getLegendData?.() ?? null); + } + _disconnectViewer() { this._viewer?.removeEventListener('legend-data-changed', this._onData); this._viewer = null; } + _render(data) { + if (!data || this._viewer?.hasAttribute('no-legend')) { this._panel.hidden = true; this._panel.replaceChildren(); return; } + const doc = this.ownerDocument, fragment = doc.createDocumentFragment(); + const title = doc.createElement('h4'); title.textContent = 'Zones'; fragment.append(title); + for (const zone of data.zones) { + const label = doc.createElement('label'), checkbox = doc.createElement('input'); + checkbox.type = 'checkbox'; checkbox.checked = zone.visible; + checkbox.addEventListener('change', () => this._viewer?.setZoneVisible(zone.name, checkbox.checked)); + const swatch = doc.createElement('span'); swatch.className = 'sw'; swatch.style.backgroundColor = zone.color; + label.append(checkbox, swatch, doc.createTextNode(zone.name + ' ')); + const count = doc.createElement('span'); count.className = 'muted'; count.textContent = '(' + zone.count + ')'; label.append(count); fragment.append(label); + } + if (data.keys.length) { + const heading = doc.createElement('h4'); heading.textContent = 'Légende'; fragment.append(heading); + for (const key of data.keys) { const row = doc.createElement('div'); row.className = 'key'; const swatch = doc.createElement('span'); swatch.className = 'sw'; swatch.style.backgroundColor = key.color; row.append(swatch, doc.createTextNode(key.label)); fragment.append(row); } + } + this._panel.replaceChildren(fragment); this._panel.hidden = false; + } +} +if (!customElements.get('idf-legend')) customElements.define('idf-legend', IdfLegend); +export default IdfLegend; From 17ac910bd4bcb3959c2ddb03e9a89bc291b13b30 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Sat, 31 Oct 2026 20:42:56 +0100 Subject: [PATCH 3/7] Add idf-legend to default layout --- _layouts/default.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/_layouts/default.html b/_layouts/default.html index ec458b4..fc79a6e 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -12,10 +12,14 @@

- +
+ + +
+ From f6b6883e42cf4adecf981aee14f04db4dd3d5777 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Sat, 31 Oct 2026 22:29:14 +0100 Subject: [PATCH 7/7] Display IDF zones and color legend in two columns --- lib/modules/components/idf-legend.js | 76 ++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/lib/modules/components/idf-legend.js b/lib/modules/components/idf-legend.js index ad5836c..3b24997 100644 --- a/lib/modules/components/idf-legend.js +++ b/lib/modules/components/idf-legend.js @@ -15,13 +15,25 @@ const TEMPLATE = /* html */ ` display: none; } .panel { - min-width: 140px; - padding: 6px 8px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + min-width: 300px; + padding: 8px 10px; background: rgba(255, 255, 255, .92); border-radius: 6px; box-shadow: 0 1px 4px rgba(0, 0, 0, .18); backdrop-filter: blur(4px); } + .column { + min-width: 0; + } + @media (max-width: 480px) { + .panel { + grid-template-columns: 1fr; + min-width: 0; + } + } h4 { margin: 0 0 4px; font-size: 12px; @@ -78,40 +90,62 @@ export class IdfLegend extends HTMLElement { } _disconnectViewer() { this._viewer?.removeEventListener('legend-data-changed', this._onData); this._viewer = null; } _render(data) { - if (!data || this._viewer?.hasAttribute('no-legend')) { this._panel.hidden = true; this._panel.replaceChildren(); return; } - const doc = this.ownerDocument, fragment = doc.createDocumentFragment(); - const title = doc.createElement('h4'); title.textContent = 'Zones'; fragment.append(title); + if (!data || this._viewer?.hasAttribute('no-legend')) { + this._panel.hidden = true; + this._panel.replaceChildren(); + return; + } + + const doc = this.ownerDocument; + const zonesColumn = doc.createElement('section'); + zonesColumn.className = 'column'; + const zonesTitle = doc.createElement('h4'); + zonesTitle.textContent = 'Zones'; + zonesColumn.append(zonesTitle); + for (const zone of data.zones) { - const label = doc.createElement('label'), checkbox = doc.createElement('input'); - checkbox.type = 'checkbox'; checkbox.checked = zone.visible; + const label = doc.createElement('label'); + const checkbox = doc.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = zone.visible; checkbox.addEventListener('change', () => { this._viewer?.setZoneVisible(zone.name, checkbox.checked); }); - const swatch = doc.createElement('span'); swatch.className = 'sw'; swatch.style.backgroundColor = zone.color; + + const swatch = doc.createElement('span'); + swatch.className = 'sw'; + swatch.style.backgroundColor = zone.color; label.append( checkbox, swatch, doc.createTextNode(zone.name + ' ') ); + const count = doc.createElement('span'); count.className = 'muted'; count.textContent = '(' + zone.count + ')'; label.append(count); - fragment.append(label); + zonesColumn.append(label); } - if (data.keys.length) { - const heading = doc.createElement('h4'); heading.textContent = 'Légende'; fragment.append(heading); - for (const key of data.keys) { - const row = doc.createElement('div'); - row.className = 'key'; - const swatch = doc.createElement('span'); - swatch.className = 'sw'; - swatch.style.backgroundColor = key.color; - row.append(swatch, doc.createTextNode(key.label)); - fragment.append(row); - } + + const keysColumn = doc.createElement('section'); + keysColumn.className = 'column'; + const keysTitle = doc.createElement('h4'); + keysTitle.textContent = 'Légende'; + keysColumn.append(keysTitle); + + for (const key of data.keys) { + const row = doc.createElement('div'); + row.className = 'key'; + const swatch = doc.createElement('span'); + swatch.className = 'sw'; + swatch.style.backgroundColor = key.color; + row.append(swatch, doc.createTextNode(key.label)); + keysColumn.append(row); } - this._panel.replaceChildren(fragment); this._panel.hidden = false; + + this._panel.replaceChildren(zonesColumn, keysColumn); + this._panel.hidden = false; } } if (!customElements.get('idf-legend')) customElements.define('idf-legend', IdfLegend);