Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions _layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@
<div class="row">
<div class="col">
<h1 class="display-5" data-i18n="home.master">
Maitrisez le fonctionnement de votre bâtiment à travers son jumeau numérique
Maitrisez le fonctionnement de votre bâtiment à travers son jumeau numérique
</h1>
<idf-legend viewer="building"></idf-legend>
</div>
<div class="col-sm">
<idf-viewer src="/models/example.idf" style="height: 500px"></idf-viewer>
<idf-viewer id="building" src="/models/example.idf" style="height: 500px"></idf-viewer>
</div>
</main>
<script type="module" src="/lib/modules/components/idf-viewer.js"></script>
<script type="module" src="/lib/modules/components/idf-legend.js"></script>
<script>
const models = [
'example.idf',
Expand Down
152 changes: 152 additions & 0 deletions lib/modules/components/idf-legend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/** Standalone legend for an <idf-viewer> element. */
const TEMPLATE = /* html */ `
<style>
:host {
display: block;
z-index: 3;
position: static;
max-width: 100%;
overflow: auto;
font: 13px/1.4 system-ui, -apple-system, "Segoe UI", sans-serif;
color: #1f2328;
}
:host([hidden]),
.panel[hidden] {
display: none;
}
.panel {
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;
text-transform: uppercase;
letter-spacing: .04em;
color: #57606a;
}
h4 + h4 {
margin-top: 8px;
}
label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
padding: 1px 0;
}
.sw {
width: 12px;
height: 12px;
border-radius: 3px;
border: 1px solid rgba(0, 0, 0, .25);
flex: none;
}
.key {
display: flex;
align-items: center;
gap: 6px;
padding: 1px 0;
color: #444;
}
.muted {
color: #8b949e;
}
</style><div class="panel" hidden></div>`;

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;
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');
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;
label.append(
checkbox,
swatch,
doc.createTextNode(zone.name + ' ')
);

const count = doc.createElement('span');
count.className = 'muted';
count.textContent = '(' + zone.count + ')';
label.append(count);
zonesColumn.append(label);
}

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(zonesColumn, keysColumn);
this._panel.hidden = false;
}
}
if (!customElements.get('idf-legend')) customElements.define('idf-legend', IdfLegend);
export default IdfLegend;
116 changes: 47 additions & 69 deletions lib/modules/components/idf-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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; }
</style>
<div class="canvas-host"></div>
Expand All @@ -173,7 +166,6 @@ const TEMPLATE = /* html */ `
<label><input type="checkbox" name="windows" checked> Vitrages</label>
<button type="button" name="fit" title="Recentrer la vue">Recentrer</button>
</div>
<div class="panel legend" hidden></div>
<div class="tooltip" hidden></div>
<div class="empty">Déposez un fichier .idf ici<br>ou utilisez « Ouvrir un IDF… »</div>
<div class="status" hidden></div>
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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 ------------------------------------------------------
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -583,7 +574,7 @@ export class IdfViewer extends HTMLElement {

this._applyColors();
this._applyVisibility();
this._refreshLegend();
this._emitLegendData();
this.fitView();
}

Expand Down Expand Up @@ -692,65 +683,52 @@ 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;
/** 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],
];
}
const parts = [];
const hex = (c) => `#${c.toString(16).padStart(6, '0')}`;

parts.push('<h4>Zones</h4>');
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(
`<label><input type="checkbox" data-zone="${escapeHTML(z.name)}" ${checked}>` +
`<span class="sw" style="background:${color}"></span>${escapeHTML(z.name)} <span class="muted">(${count})</span></label>`
);
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],
];
}
let keys = [];

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;
if (this._model.fenestrations.length) {
keys.push(
['Fenêtre', TYPE_COLORS.window],
['Porte vitrée', TYPE_COLORS.glassdoor],
['Porte', TYPE_COLORS.door],
);
}
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('<h4>Légende</h4>');
for (const [label, color] of all) parts.push(`<div class="key"><span class="sw" style="background:${hex(color)}"></span>${label}</div>`);
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) })) };
}

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) {
Expand Down
Loading