{"id":15404,"date":"2026-10-17T07:29:12","date_gmt":"2026-10-17T07:29:12","guid":{"rendered":"https:\/\/hondellighting.com\/?page_id=15404"},"modified":"2026-10-17T07:50:56","modified_gmt":"2026-10-17T07:50:56","slug":"real-time-linear-wall-washer-grazer-simulator","status":"publish","type":"page","link":"https:\/\/hondellighting.com\/ar\/real-time-linear-wall-washer-grazer-simulator.html","title":{"rendered":"Real-Time Linear Wall Washer &#038; Grazer Simulator"},"content":{"rendered":"<div data-elementor-type=\"wp-page\" data-elementor-id=\"15404\" class=\"elementor elementor-15404\">\n\t\t\t\t<div class=\"elementor-element elementor-element-6f25258 e-flex e-con-boxed e-con e-parent\" data-id=\"6f25258\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-ccd77f7 elementor-widget elementor-widget-spacer\" data-id=\"ccd77f7\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-ccfc055 elementor-widget elementor-widget-heading\" data-id=\"ccfc055\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Real-Time Linear Wall Washer &amp; Grazer Simulator<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-4d71da0 elementor-widget elementor-widget-text-editor\" data-id=\"4d71da0\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p data-path-to-node=\"3,1\"><span class=\"selected\">Stop guessing optical reach on high-rise facades. Simulate vertical illuminance (<\/span>$E_v$<span class=\"selected\">) on textured architectural surfaces from <\/span>$10\\text{ m}$<span class=\"selected\"> heights down to base level. Test asymmetric grazing optics (<\/span>$10^\\circ \\times 30^\\circ$<span class=\"selected\">, <\/span>$15^\\circ \\times 65^\\circ$<span class=\"selected\">), fine-tune setback distances, and analyze live DIALux false-color heatmaps\u2014all inside your browser.<\/span><\/p><p data-path-to-node=\"3,2\">I built this lightweight simulator as a <b data-path-to-node=\"3,2\" data-index-in-node=\"40\">fun, interactive playground for beginners and clients to experiment with lighting concepts before diving into heavy DIALux calculations.<\/b> Drag lights, adjust luminous efficacy, see solid-wall occlusion in action, and get instant lux feedback\u2014complete with one-click PDF reports to guide your final engineering plans.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-815b0f1 e-con-full e-flex e-con e-parent\" data-id=\"815b0f1\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-1142041 elementor-widget elementor-widget-html\" data-id=\"1142041\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n  <title>DIALux Outdoor Linear Wall Washer & Grazer Simulator (10m x 5m)<\/title>\r\n\r\n  <!-- Tailwind CSS CDN -->\r\n  <script src=\"https:\/\/cdn.tailwindcss.com\"><\/script>\r\n  \r\n  <!-- Font Awesome Icons -->\r\n  <link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/font-awesome\/6.5.1\/css\/all.min.css\">\r\n  \r\n  <!-- Inter & JetBrains Mono Fonts -->\r\n  <link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\r\n  <link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\r\n  <link href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;600;700&display=swap\" rel=\"stylesheet\">\r\n\r\n  <!-- jsPDF & html2canvas for PDF Report Generation -->\r\n  <script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jspdf\/2.5.1\/jspdf.umd.min.js\"><\/script>\r\n  <script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/html2canvas\/1.4.1\/html2canvas.min.js\"><\/script>\r\n\r\n  <style>\r\n    body {\r\n      font-family: 'Inter', sans-serif;\r\n      user-select: none;\r\n      -webkit-user-select: none;\r\n    }\r\n    .font-mono {\r\n      font-family: 'JetBrains Mono', monospace;\r\n    }\r\n    ::-webkit-scrollbar {\r\n      width: 6px;\r\n      height: 6px;\r\n    }\r\n    ::-webkit-scrollbar-track {\r\n      background: #020617;\r\n    }\r\n    ::-webkit-scrollbar-thumb {\r\n      background: #1e293b;\r\n      border-radius: 4px;\r\n    }\r\n    ::-webkit-scrollbar-thumb:hover {\r\n      background: #334155;\r\n    }\r\n    input[type=range] {\r\n      accent-color: #f59e0b;\r\n    }\r\n  <\/style>\r\n<\/head>\r\n<body class=\"bg-[#030712] text-slate-100 min-h-screen flex flex-col overflow-x-hidden antialiased\">\r\n\r\n  <!-- Header -->\r\n  <header class=\"bg-slate-950\/95 backdrop-blur-md border-b border-slate-800\/80 px-4 py-2.5 sticky top-0 z-40 flex items-center justify-between gap-4\">\r\n    <div class=\"flex items-center gap-3\">\r\n      <div class=\"w-9 h-9 rounded-xl bg-gradient-to-tr from-amber-500 to-amber-300 flex items-center justify-center text-slate-950 shadow-lg shadow-amber-500\/20\">\r\n        <i class=\"fa-solid fa-bars-staggered text-lg\"><\/i>\r\n      <\/div>\r\n      <div>\r\n        <h1 class=\"text-sm md:text-base font-bold text-white tracking-tight flex items-center gap-2\">\r\n          Outdoor Linear Wall Washer & Grazer Simulator\r\n          <span class=\"text-[10px] uppercase font-mono px-2 py-0.5 rounded-full bg-amber-500\/20 text-amber-300 border border-amber-500\/40\">DIALux Photometric Core<\/span>\r\n        <\/h1>\r\n        <p class=\"text-[11px] text-slate-400\">10m Height \u00d7 5m Width Textured Facade \u00b7 Bottom Ground-Grazing Engine \u00b7 IESNA \/ CIE Standards<\/p>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <!-- Quick Facade Stats -->\r\n    <div class=\"hidden lg:flex items-center gap-5 text-xs bg-slate-900\/90 px-4 py-1.5 rounded-xl border border-slate-800 font-mono\">\r\n      <div class=\"flex items-center gap-2\">\r\n        <span class=\"w-2 h-2 rounded-full bg-amber-400 animate-pulse\"><\/span>\r\n        <span class=\"text-slate-400\">Wall:<\/span>\r\n        <span class=\"text-white font-bold\">10m (H) \u00d7 5m (W)<\/span>\r\n      <\/div>\r\n      <div class=\"w-px h-4 bg-slate-800\"><\/div>\r\n      <div>\r\n        <span class=\"text-slate-400\">Linear Run:<\/span>\r\n        <span class=\"text-amber-400 font-bold\" id=\"statLinearLength\">5 \u00d7 1m (5m continuous)<\/span>\r\n      <\/div>\r\n      <div class=\"w-px h-4 bg-slate-800\"><\/div>\r\n      <div>\r\n        <span class=\"text-slate-400\">Load:<\/span>\r\n        <span class=\"text-emerald-400 font-bold\" id=\"statTotalLoad\">180 W (19,800 lm)<\/span>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <!-- Actions -->\r\n    <div class=\"flex items-center gap-2\">\r\n      <select id=\"presetOpticSelect\" class=\"bg-slate-900 border border-slate-700 text-xs rounded-lg px-2.5 py-1.5 text-slate-200 outline-none focus:border-amber-500\">\r\n        <option value=\"graze_tall\">Preset: 10m High-Rise Graze (10\u00b0\u00d730\u00b0 Asymmetric)<\/option>\r\n        <option value=\"narrow_pencil\">Preset: Dramatic Narrow Pencil (8\u00b0 Pin-Spot)<\/option>\r\n        <option value=\"even_wash\">Preset: Smooth Wide Wash (15\u00b0\u00d765\u00b0 Optical)<\/option>\r\n        <option value=\"heavy_flood\">Preset: Maximum Intensity (60W 45\u00b0)<\/option>\r\n      <\/select>\r\n\r\n      <button id=\"exportPdfBtn\" class=\"bg-amber-500 hover:bg-amber-400 text-slate-950 font-semibold px-3 py-1.5 rounded-lg text-xs flex items-center gap-1.5 shadow-md shadow-amber-500\/20 transition active:scale-95\">\r\n        <i class=\"fa-solid fa-file-pdf\"><\/i>\r\n        <span>PDF Report<\/span>\r\n      <\/button>\r\n    <\/div>\r\n  <\/header>\r\n\r\n  <main class=\"flex-1 grid grid-cols-1 xl:grid-cols-12 gap-0 overflow-hidden\">\r\n    \r\n    <!-- Viewport Area (Left 8-9 cols) -->\r\n    <div class=\"xl:col-span-8 2xl:col-span-9 bg-[#030712] flex flex-col border-b xl:border-b-0 xl:border-r border-slate-800\/80 relative\">\r\n      \r\n      <!-- Viewport Toolbar -->\r\n      <div class=\"bg-slate-950\/90 backdrop-blur-md px-4 py-2 border-b border-slate-800\/80 flex flex-wrap items-center justify-between gap-3 text-xs z-20\">\r\n        <div class=\"flex items-center gap-1.5 bg-slate-900 p-1 rounded-lg border border-slate-800\">\r\n          <button id=\"viewRealisticBtn\" class=\"px-3 py-1.5 rounded-md font-medium transition flex items-center gap-1.5 bg-slate-800 text-amber-300 shadow-sm\">\r\n            <i class=\"fa-solid fa-eye\"><\/i> Realistic Brick Graze\r\n          <\/button>\r\n          <button id=\"viewHeatmapBtn\" class=\"px-3 py-1.5 rounded-md font-medium text-slate-400 hover:text-white transition flex items-center gap-1.5\">\r\n            <i class=\"fa-solid fa-fire\"><\/i> DIALux False Color\r\n          <\/button>\r\n        <\/div>\r\n\r\n        <!-- Overlays -->\r\n        <div class=\"flex items-center gap-2\">\r\n          <button id=\"toggleIsoluxBtn\" class=\"px-2.5 py-1.5 rounded-lg bg-amber-500\/20 text-amber-300 border border-amber-500\/40 transition flex items-center gap-1.5 font-medium\" title=\"Toggle Live DIALux Isolux Contour Lines\">\r\n            <i class=\"fa-solid fa-chart-line\"><\/i> <span>Isolux Curves<\/span>\r\n          <\/button>\r\n          <button id=\"toggleBrickTextureBtn\" class=\"px-2.5 py-1.5 rounded-lg bg-slate-900 hover:bg-slate-800 text-white border border-slate-700 transition flex items-center gap-1.5\" title=\"Toggle Relief Brick Texture\">\r\n            <i class=\"fa-solid fa-cubes-stacked\"><\/i> <span>Brick Texture<\/span>\r\n          <\/button>\r\n          <button id=\"toggleGridBtn\" class=\"px-2.5 py-1.5 rounded-lg bg-slate-900 hover:bg-slate-800 text-amber-400 border border-slate-800 transition flex items-center gap-1.5\" title=\"Toggle 1-Meter Facade Grid\">\r\n            <i class=\"fa-solid fa-border-all\"><\/i> <span>1m Grid<\/span>\r\n          <\/button>\r\n        <\/div>\r\n\r\n        <!-- Zoom & Pan Controls -->\r\n        <div class=\"flex items-center gap-1 bg-slate-900 px-2 py-1 rounded-lg border border-slate-800 font-mono text-[11px]\">\r\n          <button id=\"zoomOutBtn\" class=\"w-6 h-6 rounded hover:bg-slate-800 text-slate-300 hover:text-white flex items-center justify-center transition\" title=\"Zoom Out\">\r\n            <i class=\"fa-solid fa-minus text-[10px]\"><\/i>\r\n          <\/button>\r\n          <span id=\"zoomLevelDisplay\" class=\"text-amber-400 font-bold px-1 min-w-[42px] text-center select-none\">100%<\/span>\r\n          <button id=\"zoomInBtn\" class=\"w-6 h-6 rounded hover:bg-slate-800 text-slate-300 hover:text-white flex items-center justify-center transition\" title=\"Zoom In\">\r\n            <i class=\"fa-solid fa-plus text-[10px]\"><\/i>\r\n          <\/button>\r\n          <div class=\"w-px h-3.5 bg-slate-800 mx-0.5\"><\/div>\r\n          <button id=\"zoomResetBtn\" class=\"px-1.5 h-6 rounded hover:bg-slate-800 text-slate-400 hover:text-white flex items-center justify-center transition\" title=\"Reset View (100%)\">\r\n            <i class=\"fa-solid fa-arrows-rotate text-[10px]\"><\/i>\r\n          <\/button>\r\n        <\/div>\r\n\r\n        <!-- Real-Time Cursor Lux Probe Display -->\r\n        <div class=\"flex items-center gap-2 bg-slate-900 px-3 py-1 rounded-lg border border-slate-800 font-mono text-[11px]\">\r\n          <span class=\"text-slate-400\"><i class=\"fa-solid fa-crosshairs text-amber-400 mr-1\"><\/i>Probe:<\/span>\r\n          <span id=\"probeLuxValue\" class=\"text-white font-bold\">245 lx<\/span>\r\n          <span class=\"text-slate-600\">|<\/span>\r\n          <span id=\"probeCoords\" class=\"text-amber-300 font-medium\">H: 3.2m, X: 2.5m<\/span>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <!-- Canvas Area -->\r\n      <div class=\"relative flex-1 flex items-center justify-center p-3 bg-[#020617] overflow-auto\">\r\n        <canvas id=\"lightingCanvas\" width=\"900\" height=\"1100\" class=\"rounded-xl shadow-2xl border border-slate-800 cursor-crosshair max-w-full h-auto object-contain\"><\/canvas>\r\n        \r\n        <!-- Live Full-Spectrum DIALux False Color Legend Bar -->\r\n        <div id=\"heatmapLegendBar\" class=\"hidden absolute bottom-6 right-6 bg-slate-950\/95 backdrop-blur-md p-3.5 rounded-xl border border-slate-700\/80 shadow-2xl z-20 font-mono text-[10px] space-y-1.5\">\r\n          <div class=\"text-[11px] font-bold text-white mb-1 flex items-center justify-between\">\r\n            <span class=\"flex items-center gap-1.5\"><i class=\"fa-solid fa-palette text-amber-400\"><\/i> Facade Vertical Lux<\/span>\r\n            <span class=\"text-slate-300\">Lux (lx)<\/span>\r\n          <\/div>\r\n          <!-- Calibrated 8-color DIALux spectrum for facade illuminance -->\r\n          <div class=\"w-56 h-4 rounded-md border border-slate-600 shadow-inner\" style=\"background: linear-gradient(to right, #110038 0%, #1e3a8a 12%, #0284c7 25%, #059669 40%, #84cc16 55%, #facc15 70%, #f97316 85%, #dc2626 95%, #f43f5e 100%);\"><\/div>\r\n          <div class=\"flex justify-between text-white font-semibold text-[10px] pt-0.5\">\r\n            <span>&lt;10<\/span>\r\n            <span>25<\/span>\r\n            <span>50<\/span>\r\n            <span>100<\/span>\r\n            <span>200<\/span>\r\n            <span>350<\/span>\r\n            <span>500+<\/span>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <!-- Interactive Drag Tip -->\r\n        <div class=\"absolute bottom-5 left-5 bg-slate-950\/90 backdrop-blur-md px-3 py-1.5 rounded-lg border border-slate-800 text-[11px] text-white pointer-events-none hidden sm:flex items-center gap-2\">\r\n          <i class=\"fa-solid fa-arrows-up-down-left-right text-amber-400\"><\/i>\r\n          <span>Click & drag linear washer luminaires at base \u00b7 Scroll wheel or pinch to zoom \u00b7 Drag canvas to pan<\/span>\r\n        <\/div>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <!-- Sidebar Control Panel (Right 3-4 cols) -->\r\n    <div class=\"xl:col-span-4 2xl:col-span-3 bg-slate-950 flex flex-col h-full border-slate-800 overflow-y-auto\">\r\n      \r\n      <!-- Tabs Switcher -->\r\n      <div class=\"flex border-b border-slate-800 p-2 gap-1 bg-slate-950 sticky top-0 z-10 text-xs\">\r\n        <button id=\"tabControlsBtn\" class=\"flex-1 py-2 font-medium rounded-md text-amber-300 bg-slate-900 flex items-center justify-center gap-2 shadow-sm border border-slate-800\">\r\n          <i class=\"fa-solid fa-sliders\"><\/i> Washer Parameters\r\n        <\/button>\r\n        <button id=\"tabMetricsBtn\" class=\"flex-1 py-2 font-medium rounded-md text-slate-400 hover:text-white flex items-center justify-center gap-2\">\r\n          <i class=\"fa-solid fa-chart-column\"><\/i> Facade Photometrics\r\n        <\/button>\r\n      <\/div>\r\n\r\n      <!-- Tab Content: Parameters -->\r\n      <div id=\"tabContentControls\" class=\"p-4 space-y-4\">\r\n        \r\n        <!-- Luminaire Specification Card -->\r\n        <div class=\"bg-slate-900\/90 p-4 rounded-xl border border-amber-500\/50 space-y-3.5 shadow-xl\">\r\n          <div class=\"flex items-center justify-between border-b border-slate-800 pb-2\">\r\n            <span class=\"text-xs font-bold text-amber-300 flex items-center gap-1.5\">\r\n              <i class=\"fa-solid fa-bars-progress text-amber-400\"><\/i>\r\n              Linear Luminaire Profile\r\n            <\/span>\r\n            <button id=\"toggleMasterPowerBtn\" class=\"text-xs px-2.5 py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 transition\" title=\"Toggle Power\">\r\n              <i class=\"fa-solid fa-power-off text-xs text-emerald-400 mr-1\"><\/i> Power ON\r\n            <\/button>\r\n          <\/div>\r\n\r\n          <!-- Wattage Selection: 18W, 24W, 36W, 48W, 60W -->\r\n          <div class=\"space-y-1.5\">\r\n            <div class=\"flex justify-between items-center text-xs\">\r\n              <span class=\"text-slate-300 font-medium\">Power Rating (per 1m module):<\/span>\r\n              <span class=\"font-mono font-bold text-amber-400 text-sm\" id=\"valWattage\">36 W<\/span>\r\n            <\/div>\r\n            <div class=\"grid grid-cols-5 gap-1.5\">\r\n              <button onclick=\"setWattage(18)\" class=\"watt-btn py-1.5 text-xs rounded bg-slate-800 hover:bg-slate-700 text-slate-300 font-mono transition\">18 \u0648\u0627\u0637<\/button>\r\n              <button onclick=\"setWattage(24)\" class=\"watt-btn py-1.5 text-xs rounded bg-slate-800 hover:bg-slate-700 text-slate-300 font-mono transition\">24 \u0648\u0627\u0637<\/button>\r\n              <button onclick=\"setWattage(36)\" class=\"watt-btn py-1.5 text-xs rounded bg-amber-600 text-white font-bold font-mono transition\">36 \u0648\u0627\u0637<\/button>\r\n              <button onclick=\"setWattage(48)\" class=\"watt-btn py-1.5 text-xs rounded bg-slate-800 hover:bg-slate-700 text-slate-300 font-mono transition\">48W<\/button>\r\n              <button onclick=\"setWattage(60)\" class=\"watt-btn py-1.5 text-xs rounded bg-slate-800 hover:bg-slate-700 text-slate-300 font-mono transition\">60W<\/button>\r\n            <\/div>\r\n          <\/div>\r\n\r\n          <!-- Symmetrical & Asymmetrical Beam Angle Selection -->\r\n          <div class=\"space-y-2\">\r\n            <div class=\"flex justify-between items-center text-xs\">\r\n              <span class=\"text-slate-300 font-medium\">Optical Beam Spread:<\/span>\r\n              <span class=\"font-mono font-bold text-emerald-400\" id=\"valBeamAngle\">10\u00b0 \u00d7 30\u00b0 Asymmetric<\/span>\r\n            <\/div>\r\n            \r\n            <!-- Circular \/ Symmetric -->\r\n            <div>\r\n              <span class=\"text-[10px] text-slate-400 uppercase tracking-wider block mb-1\">Circular \/ Narrow \/ Wide Spot:<\/span>\r\n              <div class=\"grid grid-cols-6 gap-1 text-[11px] font-mono\">\r\n                <button onclick=\"setBeam('8')\" class=\"beam-btn py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-300\">8\u00b0<\/button>\r\n                <button onclick=\"setBeam('10')\" class=\"beam-btn py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-300\">10\u00b0<\/button>\r\n                <button onclick=\"setBeam('15')\" class=\"beam-btn py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-300\">15\u00b0<\/button>\r\n                <button onclick=\"setBeam('45')\" class=\"beam-btn py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-300\">45\u00b0<\/button>\r\n                <button onclick=\"setBeam('60')\" class=\"beam-btn py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-300\">60\u00b0<\/button>\r\n                <button onclick=\"setBeam('80')\" class=\"beam-btn py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-300\">80\u00b0<\/button>\r\n              <\/div>\r\n            <\/div>\r\n\r\n            <!-- Asymmetrical \/ Elliptical Grazing Optics -->\r\n            <div>\r\n              <span class=\"text-[10px] text-slate-400 uppercase tracking-wider block mb-1\">Asymmetric \/ Linear Wall Grazer:<\/span>\r\n              <div class=\"grid grid-cols-2 gap-1 text-[11px] font-mono\">\r\n                <button onclick=\"setBeam('10x30')\" class=\"beam-btn py-1.5 rounded bg-amber-600 text-white font-bold ring-1 ring-amber-400\">10\u00b0 \u00d7 30\u00b0<\/button>\r\n                <button onclick=\"setBeam('20x45')\" class=\"beam-btn py-1.5 rounded bg-slate-800 hover:bg-slate-700 text-slate-300\">20\u00b0 \u00d7 45\u00b0<\/button>\r\n                <button onclick=\"setBeam('15x65')\" class=\"beam-btn py-1.5 rounded bg-slate-800 hover:bg-slate-700 text-slate-300\">15\u00b0 \u00d7 65\u00b0<\/button>\r\n                <button onclick=\"setBeam('15x45')\" class=\"beam-btn py-1.5 rounded bg-slate-800 hover:bg-slate-700 text-slate-300\">15\u00b0 \u00d7 45\u00b0<\/button>\r\n              <\/div>\r\n            <\/div>\r\n          <\/div>\r\n\r\n          <!-- Setback Distance (Offset from wall base) -->\r\n          <div class=\"space-y-1\">\r\n            <div class=\"flex justify-between items-center text-xs\">\r\n              <span class=\"text-slate-300 flex items-center gap-1\">\r\n                <span>Setback Distance (Offset from Wall):<\/span>\r\n                <i class=\"fa-solid fa-circle-info text-[10px] text-slate-400\" title=\"Distance from wall base to fixture centerline. 0.1-0.2m for tight grazing, 0.4-0.8m for uniform flood washing.\"><\/i>\r\n              <\/span>\r\n              <span class=\"font-mono text-white font-bold\" id=\"valSetback\">0.25 m<\/span>\r\n            <\/div>\r\n            <input type=\"range\" id=\"inputSetback\" min=\"0.05\" max=\"1.0\" step=\"0.05\" value=\"0.25\" class=\"w-full accent-amber-500 bg-slate-800 rounded-lg h-2 cursor-pointer\">\r\n            <div class=\"flex justify-between text-[10px] text-slate-500 font-mono\">\r\n              <span>0.05m (Tight Graze)<\/span>\r\n              <span>0.5m<\/span>\r\n              <span>1.0m (Wide Wash)<\/span>\r\n            <\/div>\r\n          <\/div>\r\n\r\n          <!-- Tilt \/ Aiming Angle -->\r\n          <div class=\"space-y-1\">\r\n            <div class=\"flex justify-between items-center text-xs\">\r\n              <span class=\"text-slate-300\">Aiming Tilt Toward Wall:<\/span>\r\n              <span class=\"font-mono text-white font-bold\" id=\"valTilt\">8\u00b0<\/span>\r\n            <\/div>\r\n            <input type=\"range\" id=\"inputTilt\" min=\"0\" max=\"25\" step=\"1\" value=\"8\" class=\"w-full accent-amber-500 bg-slate-800 rounded-lg h-2 cursor-pointer\">\r\n          <\/div>\r\n\r\n          <!-- Editable Luminous Efficacy -->\r\n          <div class=\"space-y-1\">\r\n            <div class=\"flex justify-between items-center text-xs\">\r\n              <span class=\"text-slate-300\">Luminous Efficacy:<\/span>\r\n              <div class=\"flex items-center gap-1\">\r\n                <input type=\"number\" id=\"numEfficacy\" min=\"50\" max=\"170\" value=\"110\" class=\"w-14 bg-slate-950 border border-slate-700 rounded px-1.5 py-0.5 text-right font-mono text-amber-300 text-xs focus:border-amber-500 outline-none\">\r\n                <span class=\"text-slate-400 font-mono text-[10px]\">lm\/W<\/span>\r\n              <\/div>\r\n            <\/div>\r\n            <input type=\"range\" id=\"inputEfficacy\" min=\"60\" max=\"150\" step=\"1\" value=\"110\" class=\"w-full accent-amber-400 bg-slate-800 rounded-lg h-2 cursor-pointer\">\r\n          <\/div>\r\n\r\n          <!-- Color Temperature CCT -->\r\n          <div class=\"space-y-1.5\">\r\n            <div class=\"flex justify-between text-xs\">\r\n              <span class=\"text-slate-300\">Color Temperature:<\/span>\r\n              <span class=\"font-mono text-amber-300 font-semibold\" id=\"valCCT\">3000K Warm White<\/span>\r\n            <\/div>\r\n            <div class=\"grid grid-cols-4 gap-1 text-[10px]\">\r\n              <button onclick=\"setCCT(2700)\" class=\"cct-btn py-1 rounded bg-[#ff9329]\/20 border border-[#ff9329]\/40 text-[#ffb066] font-medium\">2700 \u0623\u0644\u0641<\/button>\r\n              <button onclick=\"setCCT(3000)\" class=\"cct-btn py-1 rounded bg-[#ffb46b]\/20 border border-[#ffb46b]\/40 text-[#ffc694] font-medium ring-1 ring-amber-400\">3000K<\/button>\r\n              <button onclick=\"setCCT(4000)\" class=\"cct-btn py-1 rounded bg-[#d8ecff]\/20 border border-[#d8ecff]\/40 text-[#d8ecff] font-medium\">4000 \u0643<\/button>\r\n              <button onclick=\"setCCT(5700)\" class=\"cct-btn py-1 rounded bg-[#a4d2ff]\/20 border border-[#a4d2ff]\/40 text-[#bce0ff] font-medium\">5700K<\/button>\r\n            <\/div>\r\n          <\/div>\r\n\r\n          <!-- Total Photometrics Summary -->\r\n          <div class=\"grid grid-cols-2 gap-2 text-xs bg-slate-950 p-2.5 rounded-lg border border-slate-800\">\r\n            <div>\r\n              <span class=\"text-slate-400 block text-[10px]\">Total 5m System Flux<\/span>\r\n              <span class=\"font-mono font-bold text-emerald-400 text-sm\" id=\"valTotalLumens\">19,800 lm<\/span>\r\n            <\/div>\r\n            <div>\r\n              <span class=\"text-slate-400 block text-[10px]\">Estimated Peak Candela<\/span>\r\n              <span class=\"font-mono font-semibold text-amber-300\" id=\"valPeakCandela\">~38,500 cd<\/span>\r\n            <\/div>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <!-- Linear Run Fixture Arrangement -->\r\n        <div class=\"bg-slate-900\/80 p-3 rounded-xl border border-slate-800 space-y-2.5\">\r\n          <div class=\"flex items-center justify-between text-xs font-semibold text-white\">\r\n            <span class=\"flex items-center gap-1.5\">\r\n              <i class=\"fa-solid fa-ruler-horizontal text-amber-400\"><\/i>\r\n              Modular Arrangement (5m Base)\r\n            <\/span>\r\n            <span class=\"text-[10px] text-slate-400 font-mono\">1-Meter Segments<\/span>\r\n          <\/div>\r\n          <div class=\"flex items-center gap-2\">\r\n            <button onclick=\"setModuleCount(1)\" class=\"mod-btn flex-1 py-1.5 text-xs rounded bg-slate-950 hover:bg-slate-800 border border-slate-800 text-slate-300 font-mono\">1m (1 pcs)<\/button>\r\n            <button onclick=\"setModuleCount(3)\" class=\"mod-btn flex-1 py-1.5 text-xs rounded bg-slate-950 hover:bg-slate-800 border border-slate-800 text-slate-300 font-mono\">3m (3 pcs)<\/button>\r\n            <button onclick=\"setModuleCount(5)\" class=\"mod-btn flex-1 py-1.5 text-xs rounded bg-amber-600 text-white font-bold border border-amber-500 font-mono\">5m (Continuous)<\/button>\r\n          <\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <!-- Tab Content: Metrics -->\r\n      <div id=\"tabContentMetrics\" class=\"p-4 space-y-3 hidden\">\r\n        <div class=\"text-xs text-slate-300 bg-slate-900 p-3 rounded-lg border border-slate-800\">\r\n          <span class=\"font-semibold text-white block mb-1 flex items-center gap-1.5\">\r\n            <i class=\"fa-solid fa-building-columns text-amber-400\"><\/i> Outdoor Architectural Illumination\r\n          <\/span>\r\n          Vertical illuminance ($E_v$) evaluated on the $10\\text{ m} \\times 5\\text{ m}$ textured masonry wall. Displays elevation gradient, base-to-top contrast ratio, and uniformity.\r\n        <\/div>\r\n        \r\n        <div id=\"facadeMetricsContainer\" class=\"space-y-2.5\"><\/div>\r\n      <\/div>\r\n    <\/div>\r\n  <\/main>\r\n\r\n  <!-- Hidden PDF Export Container -->\r\n  <div id=\"pdfExportContainer\" class=\"hidden bg-slate-950 text-white p-8 max-w-4xl mx-auto space-y-6\">\r\n    <div class=\"border-b-2 border-amber-500 pb-4 flex justify-between items-end\">\r\n      <div>\r\n        <h1 class=\"text-2xl font-bold text-white\">DIALux Outdoor Linear Wall Washer Report<\/h1>\r\n        <p class=\"text-xs text-slate-400\">10.0m (H) \u00d7 5.0m (W) Exterior Masonry Facade \u00b7 Photometric Graze Simulation<\/p>\r\n      <\/div>\r\n      <div class=\"text-right text-xs text-slate-400 font-mono\">\r\n        <div>Date: <span id=\"reportDate\"><\/span><\/div>\r\n        <div>Target Facade: 10m \u00d7 5m Brick Surface<\/div>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <!-- Canvas Snapshot -->\r\n    <div class=\"border border-slate-800 rounded-lg overflow-hidden bg-[#030712] p-2\">\r\n      <div id=\"pdfCanvasImageContainer\" class=\"w-full flex justify-center\"><\/div>\r\n    <\/div>\r\n\r\n    <!-- Facade Height Lux Distribution Table -->\r\n    <div class=\"space-y-2\">\r\n      <h3 class=\"text-sm font-bold text-amber-400 border-b border-slate-800 pb-1\">Vertical Height Illuminance Gradient ($E_v$)<\/h3>\r\n      <table class=\"w-full text-xs text-left border border-slate-800\">\r\n        <thead class=\"bg-slate-900 text-slate-300 border-b border-slate-800\">\r\n          <tr>\r\n            <th class=\"p-2 border-r border-slate-800\">Height Level<\/th>\r\n            <th class=\"p-2 border-r border-slate-800\">Average Lux ($E_v$)<\/th>\r\n            <th class=\"p-2 border-r border-slate-800\">Min Lux<\/th>\r\n            <th class=\"p-2 border-r border-slate-800\">Max Lux<\/th>\r\n            <th class=\"p-2 border-r border-slate-800\">Horizontal Uniformity ($U_0$)<\/th>\r\n            <th class=\"p-2\">Visual Performance<\/th>\r\n          <\/tr>\r\n        <\/thead>\r\n        <tbody id=\"pdfGradientTableBody\"><\/tbody>\r\n      <\/table>\r\n    <\/div>\r\n\r\n    <!-- Luminaire Schedule -->\r\n    <div class=\"space-y-2\">\r\n      <h3 class=\"text-sm font-bold text-amber-400 border-b border-slate-800 pb-1\">Linear Luminaire Technical Specification<\/h3>\r\n      <table class=\"w-full text-xs text-left border border-slate-800\">\r\n        <thead class=\"bg-slate-900 text-slate-300 border-b border-slate-800\">\r\n          <tr>\r\n            <th class=\"p-2 border-r border-slate-800\">\u063a\u0631\u0636<\/th>\r\n            <th class=\"p-2 border-r border-slate-800\">Module Wattage<\/th>\r\n            <th class=\"p-2 border-r border-slate-800\">Beam Distribution<\/th>\r\n            <th class=\"p-2 border-r border-slate-800\">Setback Distance<\/th>\r\n            <th class=\"p-2 border-r border-slate-800\">Tilt Angle<\/th>\r\n            <th class=\"p-2 border-r border-slate-800\">\u0627\u0644\u062a\u062d\u0648\u064a\u0644 \u0627\u0644\u0646\u0642\u062f\u064a \u0627\u0644\u0645\u0648\u062d\u062f<\/th>\r\n            <th class=\"p-2\">System Flux<\/th>\r\n          <\/tr>\r\n        <\/thead>\r\n        <tbody id=\"pdfLuminaireTableBody\"><\/tbody>\r\n      <\/table>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <script>\r\n    \/\/ Wall and Photometric Dimensions\r\n    const WALL_HEIGHT_M = 10.0;\r\n    const WALL_WIDTH_M = 5.0;\r\n    \r\n    \/\/ Canvas dimensions & scale (900x1100)\r\n    const CANVAS_WIDTH = 900;\r\n    const CANVAS_HEIGHT = 1100;\r\n\r\n    \/\/ Viewport layout on canvas (in pixels)\r\n    \/\/ Wall will be 500px wide, 1000px high (100px per meter)\r\n    const WALL_PIX_PER_METER_Y = 100; \r\n    const WALL_PIX_PER_METER_X = 100;\r\n    const WALL_LEFT = 200;\r\n    const WALL_BOTTOM = 1040;\r\n    const WALL_TOP = 40;\r\n    const WALL_RIGHT = WALL_LEFT + WALL_WIDTH_M * WALL_PIX_PER_METER_X; \/\/ 200 + 500 = 700\r\n\r\n    \/\/ Lighting parameters\r\n    let currentWattage = 36; \/\/ 18, 24, 36, 48, 60\r\n    let currentBeam = '10x30'; \/\/ 8, 10, 15, 45, 60, 80, 10x30, 20x45, 15x65, 15x45\r\n    let currentSetback = 0.25; \/\/ meters (0.05 to 1.0)\r\n    let currentTilt = 8; \/\/ degrees towards wall (0 to 25)\r\n    let currentEfficacy = 110; \/\/ lm\/W\r\n    let currentCCT = 3000; \/\/ Kelvin\r\n    let moduleCount = 5; \/\/ 1, 3, 5 modules across 5m base\r\n    let isPowerOn = true;\r\n\r\n    \/\/ View toggles\r\n    let viewMode = 'realistic'; \/\/ 'realistic' | 'heatmap'\r\n    let showIsolux = true;\r\n    let showBrickTexture = true;\r\n    let showGrid = true;\r\n\r\n    \/\/ Zoom & Pan state\r\n    let viewZoom = 1.0;\r\n    const MIN_ZOOM = 0.5;\r\n    const MAX_ZOOM = 3.5;\r\n    let panOffset = { x: 0, y: 0 };\r\n    let isPanning = false;\r\n    let panStart = { x: 0, y: 0 };\r\n    let initialPinchDistance = null;\r\n    let pinchStartZoom = 1.0;\r\n\r\n    \/\/ Linear Fixture Array along ground (y = 0 AFF)\r\n    let fixtures = [];\r\n\r\n    function updateFixtureArray() {\r\n      fixtures = [];\r\n      const totalWidth = 5.0; \/\/ 5m base\r\n      if (moduleCount === 1) {\r\n        fixtures.push({ id: 1, x: 2.5, length: 1.0 });\r\n      } else if (moduleCount === 3) {\r\n        fixtures.push({ id: 1, x: 1.0, length: 1.0 });\r\n        fixtures.push({ id: 2, x: 2.5, length: 1.0 });\r\n        fixtures.push({ id: 3, x: 4.0, length: 1.0 });\r\n      } else {\r\n        \/\/ Continuous 5 x 1m modules\r\n        for (let i = 0; i < 5; i++) {\r\n          fixtures.push({ id: i + 1, x: 0.5 + i * 1.0, length: 1.0 });\r\n        }\r\n      }\r\n    }\r\n\r\n    let canvas = null;\r\n    let ctx = null;\r\n\r\n    function ensureCanvasContext() {\r\n      if (!canvas) canvas = document.getElementById('lightingCanvas');\r\n      if (canvas && !ctx) ctx = canvas.getContext('2d');\r\n      return !!(canvas && ctx);\r\n    }\r\n\r\n    \/\/ Optical parser\r\n    function parseBeam(beamStr) {\r\n      if (beamStr.includes('x')) {\r\n        const parts = beamStr.split('x');\r\n        return {\r\n          vert: parseFloat(parts[0]),\r\n          horiz: parseFloat(parts[1]),\r\n          isAsym: true\r\n        };\r\n      }\r\n      const val = parseFloat(beamStr);\r\n      return {\r\n        vert: val,\r\n        horiz: val,\r\n        isAsym: false\r\n      };\r\n    }\r\n\r\n    function cctToRgb(kelvin) {\r\n      const temp = kelvin \/ 100;\r\n      let r, g, b;\r\n      if (temp <= 66) {\r\n        r = 255;\r\n        g = Math.min(255, Math.max(0, 99.4708025861 * Math.log(temp) - 161.1195681661));\r\n        b = temp <= 19 ? 0 : Math.min(255, Math.max(0, 138.5177312231 * Math.log(temp - 10) - 305.0447927307));\r\n      } else {\r\n        r = Math.min(255, Math.max(0, 329.698727446 * Math.pow(temp - 60, -0.1332047592)));\r\n        g = Math.min(255, Math.max(0, 288.1221695283 * Math.pow(temp - 60, -0.0755148492)));\r\n        b = 255;\r\n      }\r\n      return { r: Math.round(r), g: Math.round(g), b: Math.round(b) };\r\n    }\r\n\r\n    \/\/ DIALux Calibrated False Color Palette\r\n    function luxToHeatmapColor(lux) {\r\n      if (lux <= 5) return 'rgb(17, 0, 56)';\r\n      if (lux < 25) {\r\n        const t = (lux - 5) \/ 20;\r\n        return `rgb(${Math.round(17 + 13 * t)}, ${Math.round(0 + 58 * t)}, ${Math.round(56 + 82 * t)})`; \/\/ to deep blue\r\n      } else if (lux < 50) {\r\n        const t = (lux - 25) \/ 25;\r\n        return `rgb(${Math.round(30 - 28 * t)}, ${Math.round(58 + 74 * t)}, ${Math.round(138 + 61 * t)})`; \/\/ to sky cyan\r\n      } else if (lux < 100) {\r\n        const t = (lux - 50) \/ 50;\r\n        return `rgb(${Math.round(2 + 3 * t)}, ${Math.round(132 + 18 * t)}, ${Math.round(199 - 94 * t)})`; \/\/ to emerald\r\n      } else if (lux < 200) {\r\n        const t = (lux - 100) \/ 100;\r\n        return `rgb(${Math.round(5 + 127 * t)}, ${Math.round(150 + 54 * t)}, ${Math.round(105 - 83 * t)})`; \/\/ to lime yellow\r\n      } else if (lux < 350) {\r\n        const t = (lux - 200) \/ 150;\r\n        return `rgb(${Math.round(132 + 117 * t)}, ${Math.round(204 - 89 * t)}, ${Math.round(22 - 1 * t)})`; \/\/ to amber orange\r\n      } else if (lux < 500) {\r\n        const t = (lux - 350) \/ 150;\r\n        return `rgb(${Math.round(249 - 29 * t)}, ${Math.round(115 - 77 * t)}, ${Math.round(21 + 17 * t)})`; \/\/ to vibrant red\r\n      } else {\r\n        const t = Math.min(1, (lux - 500) \/ 400);\r\n        return `rgb(${Math.round(220 + 24 * t)}, ${Math.round(38 + 25 * t)}, ${Math.round(38 + 56 * t)})`; \/\/ to hot magenta\r\n      }\r\n    }\r\n\r\n    \/\/ POINT-BY-POINT VERTICAL ILLUMINANCE CALCULATION (DIALux Engine for Wall Grazers)\r\n    \/\/ Ev = (I(theta, phi) * cos(alpha)) \/ d^2\r\n    \/\/ d = sqrt(setback^2 + height^2 + dx^2)\r\n    function calculateVerticalLux(wallX_m, wallH_m) {\r\n      if (!isPowerOn) return 0;\r\n      if (wallH_m < 0.05 || wallH_m > 10.0 || wallX_m < 0 || wallX_m > 5.0) return 0;\r\n\r\n      let totalEv = 0;\r\n      const optic = parseBeam(currentBeam);\r\n      const halfVertRad = (optic.vert * Math.PI) \/ 360;\r\n      const halfHorizRad = (optic.horiz * Math.PI) \/ 360;\r\n\r\n      \/\/ Solid angle approximation for peak candela\r\n      const solidAngle = Math.max(0.04, 2 * Math.PI * (1 - Math.cos(Math.min(halfVertRad, halfHorizRad))));\r\n      const lumensPerModule = currentWattage * currentEfficacy;\r\n      const peakIntensity_cd = (lumensPerModule \/ solidAngle) * 1.15;\r\n\r\n      const tiltRad = (currentTilt * Math.PI) \/ 180;\r\n\r\n      fixtures.forEach(fix => {\r\n        \/\/ Linear fixture discretized into 5 point emitters per 1m segment\r\n        const subEmitters = 5;\r\n        const subLength = fix.length \/ subEmitters;\r\n        const subLumens = lumensPerModule \/ subEmitters;\r\n        const subPeakCd = peakIntensity_cd \/ subEmitters;\r\n\r\n        for (let s = 0; s < subEmitters; s++) {\r\n          const emitX = (fix.x - fix.length \/ 2) + (s + 0.5) * subLength;\r\n          const dx = wallX_m - emitX;\r\n          const dy = wallH_m; \/\/ height above luminaire ground line\r\n          const dz = currentSetback; \/\/ distance out from wall face\r\n\r\n          \/\/ 3D Distance from emitter to calculation point on vertical wall\r\n          const dist3D = Math.sqrt(dx * dx + dy * dy + dz * dz);\r\n          if (dist3D < 0.05) continue;\r\n\r\n          \/\/ Angle of incidence with vertical wall normal (normal points outwards along +Z)\r\n          \/\/ cos(alpha) = dz \/ dist3D\r\n          const cosAlpha = dz \/ dist3D;\r\n\r\n          \/\/ Ray vector in luminaire local coordinates\r\n          \/\/ Emitter points upwards (+Y) tilted toward wall (-Z by tiltRad)\r\n          const angleFromVertical = Math.atan2(dz, dy); \/\/ angle in vertical Y-Z plane\r\n          const effectiveVerticalAngle = Math.abs(angleFromVertical - tiltRad);\r\n          const effectiveHorizontalAngle = Math.atan2(Math.abs(dx), Math.sqrt(dy * dy + dz * dz));\r\n\r\n          \/\/ Gaussian FWHM falloff in vertical and horizontal planes\r\n          const vertRel = effectiveVerticalAngle \/ halfVertRad;\r\n          const horizRel = effectiveHorizontalAngle \/ halfHorizRad;\r\n\r\n          let vertFactor = Math.exp(-Math.pow(vertRel, 2) * 1.386);\r\n          let horizFactor = Math.exp(-Math.pow(horizRel, 2) * 1.386);\r\n\r\n          \/\/ Optical cutoff wings\r\n          if (effectiveVerticalAngle > halfVertRad * 1.5) {\r\n            vertFactor *= Math.max(0, 1 - (effectiveVerticalAngle - halfVertRad * 1.5) \/ (halfVertRad * 0.4));\r\n          }\r\n          if (effectiveHorizontalAngle > halfHorizRad * 1.6) {\r\n            horizFactor *= Math.max(0, 1 - (effectiveHorizontalAngle - halfHorizRad * 1.6) \/ (halfHorizRad * 0.4));\r\n          }\r\n\r\n          const cd = subPeakCd * vertFactor * horizFactor;\r\n          \r\n          \/\/ Direct vertical lux: Ev = I * cos(alpha) \/ d^2\r\n          const directEv = (cd * cosAlpha) \/ (dist3D * dist3D);\r\n\r\n          \/\/ Inter-reflection diffuse bounce from masonry texture\r\n          const diffuseEv = (subLumens \/ 320) * Math.max(0, 1 - dist3D \/ 8.5) * cosAlpha;\r\n\r\n          totalEv += directEv + diffuseEv;\r\n        }\r\n      });\r\n\r\n      return Math.round(totalEv);\r\n    }\r\n\r\n    function drawLiveIsoluxCurves() {\r\n      if (!showIsolux || !isPowerOn) return;\r\n\r\n      const step = 14;\r\n      const xSteps = Math.floor((WALL_RIGHT - WALL_LEFT) \/ step);\r\n      const ySteps = Math.floor((WALL_BOTTOM - WALL_TOP) \/ step);\r\n\r\n      const grid = new Float32Array((xSteps + 1) * (ySteps + 1));\r\n\r\n      for (let j = 0; j <= ySteps; j++) {\r\n        const py = WALL_BOTTOM - j * step;\r\n        const wallH = (WALL_BOTTOM - py) \/ WALL_PIX_PER_METER_Y;\r\n\r\n        for (let i = 0; i <= xSteps; i++) {\r\n          const px = WALL_LEFT + i * step;\r\n          const wallX = (px - WALL_LEFT) \/ WALL_PIX_PER_METER_X;\r\n\r\n          grid[j * (xSteps + 1) + i] = calculateVerticalLux(wallX, wallH);\r\n        }\r\n      }\r\n\r\n      \/\/ Standard DIALux facade isolux threshold levels\r\n      const isoLevels = [15, 30, 50, 100, 200, 350, 500];\r\n\r\n      ctx.save();\r\n      ctx.lineWidth = 1.2;\r\n\r\n      isoLevels.forEach(level => {\r\n        ctx.strokeStyle = 'rgba(255, 255, 255, 0.75)';\r\n        ctx.beginPath();\r\n        const segments = [];\r\n\r\n        for (let j = 0; j < ySteps; j++) {\r\n          const y0 = WALL_BOTTOM - j * step;\r\n          const y1 = y0 - step;\r\n\r\n          for (let i = 0; i < xSteps; i++) {\r\n            const x0 = WALL_LEFT + i * step;\r\n            const x1 = x0 + step;\r\n\r\n            const idx = j * (xSteps + 1) + i;\r\n            const vBL = grid[idx];\r\n            const vBR = grid[idx + 1];\r\n            const vTR = grid[idx + (xSteps + 1) + 1];\r\n            const vTL = grid[idx + (xSteps + 1)];\r\n\r\n            let cellMask = 0;\r\n            if (vBL >= level) cellMask |= 1;\r\n            if (vBR >= level) cellMask |= 2;\r\n            if (vTR >= level) cellMask |= 4;\r\n            if (vTL >= level) cellMask |= 8;\r\n\r\n            if (cellMask === 0 || cellMask === 15) continue;\r\n\r\n            const getX = (v1, v2, xA, xB) => xA + (xB - xA) * ((level - v1) \/ (v2 - v1 || 1e-5));\r\n            const getY = (v1, v2, yA, yB) => yA + (yB - yA) * ((level - v1) \/ (v2 - v1 || 1e-5));\r\n\r\n            const edgeBottom = { x: getX(vBL, vBR, x0, x1), y: y0 };\r\n            const edgeRight  = { x: x1, y: getY(vBR, vTR, y0, y1) };\r\n            const edgeTop    = { x: getX(vTL, vTR, x0, x1), y: y1 };\r\n            const edgeLeft   = { x: x0, y: getY(vBL, vTL, y0, y1) };\r\n\r\n            const addSeg = (p1, p2) => {\r\n              segments.push({ p1, p2 });\r\n              ctx.moveTo(p1.x, p1.y);\r\n              ctx.lineTo(p2.x, p2.y);\r\n            };\r\n\r\n            switch (cellMask) {\r\n              case 1:  case 14: addSeg(edgeLeft, edgeBottom); break;\r\n              case 2:  case 13: addSeg(edgeBottom, edgeRight); break;\r\n              case 3:  case 12: addSeg(edgeLeft, edgeRight); break;\r\n              case 4:  case 11: addSeg(edgeRight, edgeTop); break;\r\n              case 5:           addSeg(edgeLeft, edgeTop); addSeg(edgeBottom, edgeRight); break;\r\n              case 6:  case 9:  addSeg(edgeBottom, edgeTop); break;\r\n              case 7:  case 8:  addSeg(edgeLeft, edgeTop); break;\r\n              case 10:          addSeg(edgeLeft, edgeBottom); addSeg(edgeRight, edgeTop); break;\r\n            }\r\n          }\r\n        }\r\n        ctx.stroke();\r\n\r\n        \/\/ Label isolux curves with white text pills\r\n        if (segments.length > 5) {\r\n          const stepLabel = Math.max(8, Math.floor(segments.length \/ 3));\r\n          for (let s = Math.floor(stepLabel \/ 2); s < segments.length; s += stepLabel) {\r\n            const seg = segments[s];\r\n            const mx = (seg.p1.x + seg.p2.x) \/ 2;\r\n            const my = (seg.p1.y + seg.p2.y) \/ 2;\r\n\r\n            if (mx < WALL_LEFT + 25 || mx > WALL_RIGHT - 25 || my < WALL_TOP + 25 || my > WALL_BOTTOM - 25) continue;\r\n\r\n            const textStr = `${level} lx`;\r\n            ctx.font = '700 9px JetBrains Mono';\r\n            ctx.textAlign = 'center';\r\n            ctx.textBaseline = 'middle';\r\n\r\n            const txtWidth = ctx.measureText(textStr).width;\r\n            ctx.fillStyle = '#020617';\r\n            ctx.fillRect(mx - txtWidth \/ 2 - 2, my - 6, txtWidth + 4, 12);\r\n\r\n            ctx.strokeStyle = 'rgba(255, 255, 255, 0.45)';\r\n            ctx.lineWidth = 0.8;\r\n            ctx.strokeRect(mx - txtWidth \/ 2 - 2, my - 6, txtWidth + 4, 12);\r\n\r\n            ctx.fillStyle = '#ffffff';\r\n            ctx.fillText(textStr, mx, my);\r\n          }\r\n        }\r\n      });\r\n\r\n      ctx.restore();\r\n    }\r\n\r\n    function renderBrickMasonryTexture() {\r\n      if (!showBrickTexture) {\r\n        ctx.fillStyle = '#1e293b';\r\n        ctx.fillRect(WALL_LEFT, WALL_TOP, WALL_WIDTH_M * WALL_PIX_PER_METER_X, WALL_HEIGHT_M * WALL_PIX_PER_METER_Y);\r\n        return;\r\n      }\r\n\r\n      ctx.save();\r\n      \/\/ Base rustic mortar\/stone tone\r\n      ctx.fillStyle = '#1c1f26';\r\n      ctx.fillRect(WALL_LEFT, WALL_TOP, WALL_WIDTH_M * WALL_PIX_PER_METER_X, WALL_HEIGHT_M * WALL_PIX_PER_METER_Y);\r\n\r\n      const brickH = 15;\r\n      const brickW = 40;\r\n      const totalRows = Math.floor((WALL_HEIGHT_M * WALL_PIX_PER_METER_Y) \/ brickH);\r\n      const totalCols = Math.floor((WALL_WIDTH_M * WALL_PIX_PER_METER_X) \/ brickW) + 2;\r\n\r\n      for (let r = 0; r < totalRows; r++) {\r\n        const y = WALL_TOP + r * brickH;\r\n        const isOffset = (r % 2 === 1);\r\n        const startX = WALL_LEFT - (isOffset ? brickW \/ 2 : 0);\r\n\r\n        for (let c = 0; c < totalCols; c++) {\r\n          const x = startX + c * brickW;\r\n          if (x + brickW < WALL_LEFT || x > WALL_RIGHT) continue;\r\n\r\n          \/\/ Clipped brick coordinates\r\n          const drawX = Math.max(WALL_LEFT, x + 1.5);\r\n          const drawW = Math.min(WALL_RIGHT, x + brickW - 1.5) - drawX;\r\n          const drawH = brickH - 2.5;\r\n\r\n          if (drawW <= 0) continue;\r\n\r\n          \/\/ Pseudorandom organic brick shading variation\r\n          const hash = Math.sin(r * 37.1 + c * 19.3) * 43758.5453;\r\n          const toneVar = Math.floor((hash - Math.floor(hash)) * 18);\r\n          const baseShade = 38 + toneVar;\r\n\r\n          ctx.fillStyle = `rgb(${baseShade + 2}, ${baseShade + 1}, ${baseShade})`;\r\n          ctx.fillRect(drawX, y + 1.5, drawW, drawH);\r\n\r\n          \/\/ Top highlight edge on brick (relief catching upward grazing light)\r\n          ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';\r\n          ctx.lineWidth = 0.8;\r\n          ctx.beginPath();\r\n          ctx.moveTo(drawX, y + 1.5);\r\n          ctx.lineTo(drawX + drawW, y + 1.5);\r\n          ctx.stroke();\r\n\r\n          \/\/ Bottom shadow edge\r\n          ctx.strokeStyle = 'rgba(0, 0, 0, 0.45)';\r\n          ctx.lineWidth = 1;\r\n          ctx.beginPath();\r\n          ctx.moveTo(drawX, y + 1.5 + drawH);\r\n          ctx.lineTo(drawX + drawW, y + 1.5 + drawH);\r\n          ctx.stroke();\r\n        }\r\n      }\r\n\r\n      ctx.restore();\r\n    }\r\n\r\n    function renderRealisticWallPass() {\r\n      \/\/ 1. Draw Brick Wall Surface\r\n      renderBrickMasonryTexture();\r\n\r\n      if (!isPowerOn) return;\r\n\r\n      \/\/ 2. Additive Photometric Light Pass\r\n      const lightCanvas = document.createElement('canvas');\r\n      lightCanvas.width = CANVAS_WIDTH;\r\n      lightCanvas.height = CANVAS_HEIGHT;\r\n      const lCtx = lightCanvas.getContext('2d');\r\n\r\n      lCtx.globalCompositeOperation = 'lighter';\r\n      const rgb = cctToRgb(currentCCT);\r\n      const optic = parseBeam(currentBeam);\r\n\r\n      fixtures.forEach(fix => {\r\n        const centerPixX = WALL_LEFT + fix.x * WALL_PIX_PER_METER_X;\r\n        const groundPixY = WALL_BOTTOM;\r\n\r\n        \/\/ Vertical reach based on optical narrowness and wattage\r\n        const throwHeightM = Math.min(10.5, (currentWattage \/ 8) * (85 \/ Math.max(8, optic.vert)));\r\n        const throwPixY = throwHeightM * WALL_PIX_PER_METER_Y;\r\n\r\n        \/\/ Horizontal beam spread at top\r\n        const topSpreadPix = (optic.horiz \/ 40) * 160;\r\n\r\n        lCtx.save();\r\n        lCtx.beginPath();\r\n        lCtx.rect(WALL_LEFT, WALL_TOP, WALL_WIDTH_M * WALL_PIX_PER_METER_X, WALL_HEIGHT_M * WALL_PIX_PER_METER_Y);\r\n        lCtx.clip();\r\n\r\n        \/\/ Upward grazing beam gradient\r\n        const grad = lCtx.createLinearGradient(centerPixX, groundPixY, centerPixX, groundPixY - throwPixY);\r\n        const maxAlpha = Math.min(0.96, 0.45 + (currentWattage \/ 60) * 0.5);\r\n\r\n        grad.addColorStop(0, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${maxAlpha})`);\r\n        grad.addColorStop(0.12, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${maxAlpha * 0.85})`);\r\n        grad.addColorStop(0.35, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${maxAlpha * 0.45})`);\r\n        grad.addColorStop(0.65, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${maxAlpha * 0.18})`);\r\n        grad.addColorStop(0.9, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${maxAlpha * 0.04})`);\r\n        grad.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0)`);\r\n\r\n        lCtx.fillStyle = grad;\r\n\r\n        \/\/ Asymmetric Grazing Conical Beam Shape\r\n        lCtx.beginPath();\r\n        lCtx.moveTo(centerPixX - 35, groundPixY);\r\n        lCtx.lineTo(centerPixX - topSpreadPix, groundPixY - throwPixY);\r\n        lCtx.lineTo(centerPixX + topSpreadPix, groundPixY - throwPixY);\r\n        lCtx.lineTo(centerPixX + 35, groundPixY);\r\n        lCtx.closePath();\r\n        lCtx.fill();\r\n\r\n        \/\/ Ground intense wash scallop hotspot\r\n        const scallopRadiusX = 45 + (optic.horiz \/ 2);\r\n        const scallopRadiusY = Math.min(140, 40 + currentWattage * 1.5);\r\n        const hotspotGrad = lCtx.createRadialGradient(centerPixX, groundPixY, 2, centerPixX, groundPixY, scallopRadiusY);\r\n        hotspotGrad.addColorStop(0, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${maxAlpha})`);\r\n        hotspotGrad.addColorStop(0.4, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${maxAlpha * 0.5})`);\r\n        hotspotGrad.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0)`);\r\n\r\n        lCtx.fillStyle = hotspotGrad;\r\n        lCtx.beginPath();\r\n        lCtx.ellipse(centerPixX, groundPixY, scallopRadiusX, scallopRadiusY, 0, 0, Math.PI * 2);\r\n        lCtx.fill();\r\n\r\n        lCtx.restore();\r\n      });\r\n\r\n      \/\/ Composite lighting over textured facade\r\n      ctx.save();\r\n      ctx.globalCompositeOperation = 'screen';\r\n      ctx.drawImage(lightCanvas, 0, 0);\r\n      ctx.restore();\r\n    }\r\n\r\n    function renderFalseColorHeatmap() {\r\n      const step = 12;\r\n      const xSteps = Math.floor((WALL_RIGHT - WALL_LEFT) \/ step);\r\n      const ySteps = Math.floor((WALL_BOTTOM - WALL_TOP) \/ step);\r\n\r\n      ctx.save();\r\n      ctx.fillStyle = '#030718';\r\n      ctx.fillRect(WALL_LEFT, WALL_TOP, WALL_WIDTH_M * WALL_PIX_PER_METER_X, WALL_HEIGHT_M * WALL_PIX_PER_METER_Y);\r\n\r\n      for (let j = 0; j < ySteps; j++) {\r\n        const py = WALL_BOTTOM - (j + 0.5) * step;\r\n        const wallH = (WALL_BOTTOM - py) \/ WALL_PIX_PER_METER_Y;\r\n\r\n        for (let i = 0; i < xSteps; i++) {\r\n          const px = WALL_LEFT + (i + 0.5) * step;\r\n          const wallX = (px - WALL_LEFT) \/ WALL_PIX_PER_METER_X;\r\n\r\n          const lux = calculateVerticalLux(wallX, wallH);\r\n          ctx.fillStyle = luxToHeatmapColor(lux);\r\n          ctx.fillRect(WALL_LEFT + i * step, WALL_BOTTOM - (j + 1) * step, step, step);\r\n        }\r\n      }\r\n      ctx.restore();\r\n    }\r\n\r\n    function drawArchitecturalBordersAndScale() {\r\n      ctx.save();\r\n\r\n      \/\/ Outside background darkness\r\n      ctx.fillStyle = '#060a14';\r\n      ctx.fillRect(0, 0, WALL_LEFT, CANVAS_HEIGHT);\r\n      ctx.fillRect(WALL_RIGHT, 0, CANVAS_WIDTH - WALL_RIGHT, CANVAS_HEIGHT);\r\n      ctx.fillRect(WALL_LEFT, 0, WALL_WIDTH_M * WALL_PIX_PER_METER_X, WALL_TOP);\r\n      ctx.fillRect(WALL_LEFT, WALL_BOTTOM, WALL_WIDTH_M * WALL_PIX_PER_METER_X, CANVAS_HEIGHT - WALL_BOTTOM);\r\n\r\n      \/\/ Solid architectural border outline\r\n      ctx.strokeStyle = '#475569';\r\n      ctx.lineWidth = 3;\r\n      ctx.strokeRect(WALL_LEFT, WALL_TOP, WALL_WIDTH_M * WALL_PIX_PER_METER_X, WALL_HEIGHT_M * WALL_PIX_PER_METER_Y);\r\n\r\n      \/\/ 1-Meter Grid Lines\r\n      if (showGrid) {\r\n        ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';\r\n        ctx.lineWidth = 1;\r\n\r\n        \/\/ Horizontal 1m lines (0m to 10m)\r\n        for (let m = 0; m <= 10; m++) {\r\n          const y = WALL_BOTTOM - m * WALL_PIX_PER_METER_Y;\r\n          ctx.beginPath();\r\n          ctx.moveTo(WALL_LEFT, y);\r\n          ctx.lineTo(WALL_RIGHT, y);\r\n          ctx.stroke();\r\n\r\n          \/\/ Left scale ticks & label\r\n          ctx.fillStyle = '#94a3b8';\r\n          ctx.font = '700 10px JetBrains Mono';\r\n          ctx.textAlign = 'right';\r\n          ctx.textBaseline = 'middle';\r\n          ctx.fillText(`${m}.0m`, WALL_LEFT - 12, y);\r\n\r\n          \/\/ Tick mark\r\n          ctx.strokeStyle = '#94a3b8';\r\n          ctx.beginPath();\r\n          ctx.moveTo(WALL_LEFT - 6, y);\r\n          ctx.lineTo(WALL_LEFT, y);\r\n          ctx.stroke();\r\n        }\r\n\r\n        \/\/ Vertical 1m lines (0m to 5m)\r\n        for (let x = 0; x <= 5; x++) {\r\n          const px = WALL_LEFT + x * WALL_PIX_PER_METER_X;\r\n          ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';\r\n          ctx.beginPath();\r\n          ctx.moveTo(px, WALL_TOP);\r\n          ctx.lineTo(px, WALL_BOTTOM);\r\n          ctx.stroke();\r\n\r\n          \/\/ Top dimension labels\r\n          ctx.fillStyle = '#94a3b8';\r\n          ctx.font = '700 10px JetBrains Mono';\r\n          ctx.textAlign = 'center';\r\n          ctx.textBaseline = 'bottom';\r\n          ctx.fillText(`${x}.0m`, px, WALL_TOP - 8);\r\n\r\n          ctx.strokeStyle = '#94a3b8';\r\n          ctx.beginPath();\r\n          ctx.moveTo(px, WALL_TOP - 6);\r\n          ctx.lineTo(px, WALL_TOP);\r\n          ctx.stroke();\r\n        }\r\n      }\r\n\r\n      \/\/ Elevation Title & Dimensions\r\n      ctx.fillStyle = '#ffffff';\r\n      ctx.font = '700 12px Inter';\r\n      ctx.textAlign = 'left';\r\n      ctx.fillText('FACADE ELEVATION: 10.0m (H) \u00d7 5.0m (W)', WALL_LEFT, WALL_TOP - 24);\r\n\r\n      \/\/ Ground Pavement Line \/ Trench\r\n      ctx.fillStyle = '#0f172a';\r\n      ctx.fillRect(WALL_LEFT - 40, WALL_BOTTOM, (WALL_WIDTH_M * WALL_PIX_PER_METER_X) + 80, 50);\r\n\r\n      ctx.strokeStyle = '#334155';\r\n      ctx.lineWidth = 2;\r\n      ctx.beginPath();\r\n      ctx.moveTo(WALL_LEFT - 40, WALL_BOTTOM);\r\n      ctx.lineTo(WALL_RIGHT + 40, WALL_BOTTOM);\r\n      ctx.stroke();\r\n\r\n      ctx.fillStyle = '#64748b';\r\n      ctx.font = '600 10px Inter';\r\n      ctx.textAlign = 'center';\r\n      ctx.fillText(`GROUND IN-GROUND GRAZING CHANNEL (Setback d = ${currentSetback}m, Tilt = ${currentTilt}\u00b0)`, WALL_LEFT + (WALL_WIDTH_M * WALL_PIX_PER_METER_X) \/ 2, WALL_BOTTOM + 40);\r\n\r\n      \/\/ Linear Wall Washer Luminaires at Base\r\n      const rgb = cctToRgb(currentCCT);\r\n      fixtures.forEach(fix => {\r\n        const cx = WALL_LEFT + fix.x * WALL_PIX_PER_METER_X;\r\n        const w = fix.length * WALL_PIX_PER_METER_X;\r\n        const y = WALL_BOTTOM + 8;\r\n\r\n        \/\/ Fixture Body \/ Aluminum Housing\r\n        ctx.fillStyle = '#020617';\r\n        ctx.fillRect(cx - w \/ 2, y, w, 14);\r\n\r\n        ctx.strokeStyle = isPowerOn ? '#f59e0b' : '#475569';\r\n        ctx.lineWidth = 1.5;\r\n        ctx.strokeRect(cx - w \/ 2, y, w, 14);\r\n\r\n        \/\/ LED Emitter Lens Array Strip\r\n        ctx.fillStyle = isPowerOn ? `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})` : '#334155';\r\n        if (isPowerOn) {\r\n          ctx.shadowColor = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;\r\n          ctx.shadowBlur = 10;\r\n        }\r\n        ctx.fillRect(cx - w \/ 2 + 4, y + 3, w - 8, 8);\r\n        ctx.shadowBlur = 0;\r\n\r\n        \/\/ Module Label\r\n        ctx.fillStyle = '#ffffff';\r\n        ctx.font = '700 9px JetBrains Mono';\r\n        ctx.textAlign = 'center';\r\n        ctx.fillText(`M${fix.id} (1m \u00b7 ${currentWattage}W)`, cx, y + 25);\r\n      });\r\n\r\n      ctx.restore();\r\n    }\r\n\r\n    \/\/ MAIN RENDER LOOP\r\n    function renderScene() {\r\n      if (!ensureCanvasContext()) return;\r\n\r\n      ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);\r\n\r\n      ctx.save();\r\n      \/\/ Apply zoom & pan transformation\r\n      ctx.translate(panOffset.x, panOffset.y);\r\n      ctx.scale(viewZoom, viewZoom);\r\n\r\n      if (viewMode === 'heatmap') {\r\n        renderFalseColorHeatmap();\r\n      } else {\r\n        renderRealisticWallPass();\r\n      }\r\n\r\n      \/\/ Draw Isolux Contours with crisp white labels\r\n      drawLiveIsoluxCurves();\r\n\r\n      \/\/ Draw facade frames, meters, ground channel, and fixtures\r\n      drawArchitecturalBordersAndScale();\r\n\r\n      ctx.restore();\r\n    }\r\n\r\n    function calculateFacadeMetrics() {\r\n      const gradient = [];\r\n      \/\/ Calculate levels at every 1m interval from 1m to 10m\r\n      for (let h = 1; h <= 10; h++) {\r\n        let sum = 0;\r\n        let min = Infinity;\r\n        let max = -Infinity;\r\n        let samples = 0;\r\n\r\n        for (let x = 0.2; x <= 4.8; x += 0.2) {\r\n          const lx = calculateVerticalLux(x, h);\r\n          sum += lx;\r\n          if (lx < min) min = lx;\r\n          if (lx > max) max = lx;\r\n          samples++;\r\n        }\r\n\r\n        const avg = samples > 0 ? Math.round(sum \/ samples) : 0;\r\n        min = min === Infinity ? 0 : min;\r\n        max = max === -Infinity ? 0 : max;\r\n        const u0 = avg > 0 ? (min \/ avg).toFixed(2) : '0.00';\r\n\r\n        gradient.push({\r\n          height: `${h}m`,\r\n          avg,\r\n          min,\r\n          max,\r\n          u0\r\n        });\r\n      }\r\n      return gradient;\r\n    }\r\n\r\n    function updateMetricsUI() {\r\n      const totalWattage = currentWattage * moduleCount;\r\n      const totalLumens = totalWattage * currentEfficacy;\r\n\r\n      \/\/ Header quick stats\r\n      document.getElementById('statLinearLength').textContent = `${moduleCount} \u00d7 1m (${moduleCount}m continuous)`;\r\n      document.getElementById('statTotalLoad').textContent = `${totalWattage} W (${totalLumens.toLocaleString()} lm)`;\r\n      document.getElementById('valTotalLumens').textContent = `${totalLumens.toLocaleString()} lm`;\r\n\r\n      \/\/ Candela estimate\r\n      const optic = parseBeam(currentBeam);\r\n      const halfVertRad = (optic.vert * Math.PI) \/ 360;\r\n      const halfHorizRad = (optic.horiz * Math.PI) \/ 360;\r\n      const solidAngle = Math.max(0.04, 2 * Math.PI * (1 - Math.cos(Math.min(halfVertRad, halfHorizRad))));\r\n      const estPeak = Math.round((currentWattage * currentEfficacy \/ solidAngle) * 1.15);\r\n      document.getElementById('valPeakCandela').textContent = `~${estPeak.toLocaleString()} cd`;\r\n\r\n      \/\/ Vertical Gradient Metrics Cards\r\n      const container = document.getElementById('facadeMetricsContainer');\r\n      if (!container) return;\r\n      container.innerHTML = '';\r\n\r\n      const gradient = calculateFacadeMetrics();\r\n      const keyLevels = [gradient[0], gradient[2], gradient[4], gradient[7], gradient[9]]; \/\/ 1m, 3m, 5m, 8m, 10m\r\n\r\n      keyLevels.forEach(item => {\r\n        if (!item) return;\r\n        const card = document.createElement('div');\r\n        card.className = 'p-2.5 rounded-lg border bg-slate-900 border-slate-800 text-xs space-y-1.5';\r\n        card.innerHTML = `\r\n          <div class=\"flex items-center justify-between\">\r\n            <span class=\"font-bold text-white flex items-center gap-1.5\">\r\n              <i class=\"fa-solid fa-arrows-up-down text-amber-400\"><\/i> Elevation: ${item.height} AFF\r\n            <\/span>\r\n            <span class=\"font-mono font-bold ${item.avg > 30 ? 'text-emerald-400' : 'text-amber-400'}\">${item.avg} lx (avg)<\/span>\r\n          <\/div>\r\n          <div class=\"grid grid-cols-3 gap-1.5 bg-slate-950 p-1.5 rounded font-mono text-[10px] text-slate-300\">\r\n            <div><span class=\"text-slate-500 block\">Min<\/span>${item.min} lx<\/div>\r\n            <div><span class=\"text-slate-500 block\">Max<\/span>${item.max} lx<\/div>\r\n            <div><span class=\"text-slate-500 block\">Horiz U0<\/span>${item.u0}<\/div>\r\n          <\/div>\r\n        `;\r\n        container.appendChild(card);\r\n      });\r\n    }\r\n\r\n    \/\/ Parameter setters\r\n    function setWattage(watts) {\r\n      currentWattage = watts;\r\n      document.getElementById('valWattage').textContent = `${watts} W`;\r\n\r\n      document.querySelectorAll('.watt-btn').forEach(btn => {\r\n        if (btn.textContent.trim() === `${watts}W`) {\r\n          btn.className = 'watt-btn py-1.5 text-xs rounded bg-amber-600 text-white font-bold font-mono transition';\r\n        } else {\r\n          btn.className = 'watt-btn py-1.5 text-xs rounded bg-slate-800 hover:bg-slate-700 text-slate-300 font-mono transition';\r\n        }\r\n      });\r\n\r\n      renderScene();\r\n      updateMetricsUI();\r\n    }\r\n\r\n    function setBeam(beamStr) {\r\n      currentBeam = beamStr;\r\n      const formatted = beamStr.includes('x') ? beamStr.replace('x', '\u00b0 \u00d7 ') + '\u00b0 Asymmetric' : `${beamStr}\u00b0 Symmetric`;\r\n      document.getElementById('valBeamAngle').textContent = formatted;\r\n\r\n      document.querySelectorAll('.beam-btn').forEach(btn => {\r\n        const text = btn.textContent.trim().replace('\u00b0', '').replace(' \u00d7 ', 'x').replace(' ', '');\r\n        if (text === beamStr) {\r\n          btn.className = 'beam-btn py-1.5 rounded bg-amber-600 text-white font-bold ring-1 ring-amber-400';\r\n        } else {\r\n          btn.className = 'beam-btn py-1 rounded bg-slate-800 hover:bg-slate-700 text-slate-300';\r\n        }\r\n      });\r\n\r\n      renderScene();\r\n      updateMetricsUI();\r\n    }\r\n\r\n    function setCCT(kelvin) {\r\n      currentCCT = kelvin;\r\n      const names = { 2700: '2700K Extra Warm', 3000: '3000K Warm White', 4000: '4000K Neutral', 5700: '5700K Daylight Cool' };\r\n      document.getElementById('valCCT').textContent = names[kelvin] || `${kelvin}K`;\r\n      renderScene();\r\n      updateMetricsUI();\r\n    }\r\n\r\n    function setModuleCount(count) {\r\n      moduleCount = count;\r\n      document.querySelectorAll('.mod-btn').forEach(btn => {\r\n        if (btn.textContent.startsWith(`${count}m`)) {\r\n          btn.className = 'mod-btn flex-1 py-1.5 text-xs rounded bg-amber-600 text-white font-bold border border-amber-500 font-mono';\r\n        } else {\r\n          btn.className = 'mod-btn flex-1 py-1.5 text-xs rounded bg-slate-950 hover:bg-slate-800 border border-slate-800 text-slate-300 font-mono';\r\n        }\r\n      });\r\n      updateFixtureArray();\r\n      renderScene();\r\n      updateMetricsUI();\r\n    }\r\n\r\n    function getCanvasCoords(evt) {\r\n      if (!ensureCanvasContext()) return { x: 0, y: 0, screenX: 0, screenY: 0 };\r\n      const rect = canvas.getBoundingClientRect();\r\n      const clientX = evt.clientX !== undefined ? evt.clientX : (evt.touches && evt.touches[0] ? evt.touches[0].clientX : 0);\r\n      const clientY = evt.clientY !== undefined ? evt.clientY : (evt.touches && evt.touches[0] ? evt.touches[0].clientY : 0);\r\n      const scaleX = CANVAS_WIDTH \/ rect.width;\r\n      const scaleY = CANVAS_HEIGHT \/ rect.height;\r\n\r\n      const screenX = (clientX - rect.left) * scaleX;\r\n      const screenY = (clientY - rect.top) * scaleY;\r\n\r\n      const worldX = (screenX - panOffset.x) \/ viewZoom;\r\n      const worldY = (screenY - panOffset.y) \/ viewZoom;\r\n\r\n      return { x: worldX, y: worldY, screenX, screenY };\r\n    }\r\n\r\n    function setZoomLevel(newZoom, centerX = CANVAS_WIDTH \/ 2, centerY = CANVAS_HEIGHT \/ 2) {\r\n      const clamped = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom));\r\n      if (Math.abs(clamped - viewZoom) < 0.001) return;\r\n\r\n      const factor = clamped \/ viewZoom;\r\n      panOffset.x = centerX - factor * (centerX - panOffset.x);\r\n      panOffset.y = centerY - factor * (centerY - panOffset.y);\r\n      viewZoom = clamped;\r\n\r\n      const disp = document.getElementById('zoomLevelDisplay');\r\n      if (disp) disp.textContent = `${Math.round(viewZoom * 100)}%`;\r\n      renderScene();\r\n    }\r\n\r\n    function resetZoomPan() {\r\n      viewZoom = 1.0;\r\n      panOffset = { x: 0, y: 0 };\r\n      const disp = document.getElementById('zoomLevelDisplay');\r\n      if (disp) disp.textContent = '100%';\r\n      renderScene();\r\n    }\r\n\r\n    function onPointerDown(e) {\r\n      const pos = getCanvasCoords(e);\r\n      isPanning = true;\r\n      panStart.x = pos.screenX - panOffset.x;\r\n      panStart.y = pos.screenY - panOffset.y;\r\n      if (canvas) canvas.style.cursor = 'grabbing';\r\n    }\r\n\r\n    function onPointerMove(e) {\r\n      const pos = getCanvasCoords(e);\r\n\r\n      if (isPanning) {\r\n        panOffset.x = pos.screenX - panStart.x;\r\n        panOffset.y = pos.screenY - panStart.y;\r\n        renderScene();\r\n        return;\r\n      }\r\n\r\n      \/\/ Convert world canvas coordinates to facade meters\r\n      if (pos.x >= WALL_LEFT && pos.x <= WALL_RIGHT && pos.y >= WALL_TOP && pos.y <= WALL_BOTTOM) {\r\n        const wallX = ((pos.x - WALL_LEFT) \/ WALL_PIX_PER_METER_X).toFixed(2);\r\n        const wallH = ((WALL_BOTTOM - pos.y) \/ WALL_PIX_PER_METER_Y).toFixed(2);\r\n        const lux = calculateVerticalLux(parseFloat(wallX), parseFloat(wallH));\r\n\r\n        document.getElementById('probeLuxValue').textContent = `${lux} lx`;\r\n        document.getElementById('probeCoords').textContent = `H: ${wallH}m, X: ${wallX}m`;\r\n      } else {\r\n        document.getElementById('probeLuxValue').textContent = '0 lx';\r\n        document.getElementById('probeCoords').textContent = 'Outside Wall Area';\r\n      }\r\n    }\r\n\r\n    function onPointerUp() {\r\n      if (isPanning) {\r\n        isPanning = false;\r\n        if (canvas) canvas.style.cursor = 'crosshair';\r\n      }\r\n    }\r\n\r\n    function safeAddListener(id, event, handler) {\r\n      const el = document.getElementById(id);\r\n      if (el) el.addEventListener(event, handler);\r\n    }\r\n\r\n    function setupEventListeners() {\r\n      if (!ensureCanvasContext()) return;\r\n\r\n      canvas.addEventListener('mousedown', onPointerDown);\r\n      window.addEventListener('mousemove', onPointerMove);\r\n      window.addEventListener('mouseup', onPointerUp);\r\n\r\n      canvas.addEventListener('wheel', (e) => {\r\n        e.preventDefault();\r\n        const pos = getCanvasCoords(e);\r\n        const factor = e.deltaY < 0 ? 1.12 : 0.89;\r\n        setZoomLevel(viewZoom * factor, pos.screenX, pos.screenY);\r\n      }, { passive: false });\r\n\r\n      \/\/ Touch handlers\r\n      canvas.addEventListener('touchstart', (e) => {\r\n        e.preventDefault();\r\n        if (e.touches.length === 2) {\r\n          isPanning = false;\r\n          const dx = e.touches[0].clientX - e.touches[1].clientX;\r\n          const dy = e.touches[0].clientY - e.touches[1].clientY;\r\n          initialPinchDistance = Math.hypot(dx, dy);\r\n          pinchStartZoom = viewZoom;\r\n        } else if (e.touches.length === 1) {\r\n          onPointerDown(e);\r\n        }\r\n      }, { passive: false });\r\n\r\n      window.addEventListener('touchmove', (e) => {\r\n        if (e.touches.length === 2 && initialPinchDistance) {\r\n          e.preventDefault();\r\n          const dx = e.touches[0].clientX - e.touches[1].clientX;\r\n          const dy = e.touches[0].clientY - e.touches[1].clientY;\r\n          const currentDistance = Math.hypot(dx, dy);\r\n          const rect = canvas.getBoundingClientRect();\r\n          const centerX = ((e.touches[0].clientX + e.touches[1].clientX) \/ 2 - rect.left) * (CANVAS_WIDTH \/ rect.width);\r\n          const centerY = ((e.touches[0].clientY + e.touches[1].clientY) \/ 2 - rect.top) * (CANVAS_HEIGHT \/ rect.height);\r\n          setZoomLevel(pinchStartZoom * (currentDistance \/ initialPinchDistance), centerX, centerY);\r\n        } else if (e.touches.length === 1) {\r\n          onPointerMove(e);\r\n        }\r\n      }, { passive: false });\r\n\r\n      window.addEventListener('touchend', () => {\r\n        initialPinchDistance = null;\r\n        onPointerUp();\r\n      });\r\n\r\n      \/\/ Controls\r\n      safeAddListener('inputSetback', 'input', (e) => {\r\n        currentSetback = parseFloat(e.target.value);\r\n        document.getElementById('valSetback').textContent = `${currentSetback.toFixed(2)} m`;\r\n        renderScene();\r\n        updateMetricsUI();\r\n      });\r\n\r\n      safeAddListener('inputTilt', 'input', (e) => {\r\n        currentTilt = parseInt(e.target.value);\r\n        document.getElementById('valTilt').textContent = `${currentTilt}\u00b0`;\r\n        renderScene();\r\n        updateMetricsUI();\r\n      });\r\n\r\n      safeAddListener('inputEfficacy', 'input', (e) => {\r\n        currentEfficacy = parseInt(e.target.value);\r\n        document.getElementById('numEfficacy').value = currentEfficacy;\r\n        renderScene();\r\n        updateMetricsUI();\r\n      });\r\n\r\n      safeAddListener('numEfficacy', 'change', (e) => {\r\n        currentEfficacy = Math.max(40, Math.min(180, parseInt(e.target.value) || 100));\r\n        document.getElementById('inputEfficacy').value = currentEfficacy;\r\n        renderScene();\r\n        updateMetricsUI();\r\n      });\r\n\r\n      safeAddListener('toggleMasterPowerBtn', 'click', () => {\r\n        isPowerOn = !isPowerOn;\r\n        const btn = document.getElementById('toggleMasterPowerBtn');\r\n        if (btn) {\r\n          btn.innerHTML = `<i class=\"fa-solid fa-power-off text-xs ${isPowerOn ? 'text-emerald-400' : 'text-slate-500'} mr-1\"><\/i> Power ${isPowerOn ? 'ON' : 'OFF'}`;\r\n        }\r\n        renderScene();\r\n        updateMetricsUI();\r\n      });\r\n\r\n      safeAddListener('viewRealisticBtn', 'click', () => {\r\n        viewMode = 'realistic';\r\n        document.getElementById('viewRealisticBtn').className = 'px-3 py-1.5 rounded-md font-medium transition flex items-center gap-1.5 bg-slate-800 text-amber-300 shadow-sm';\r\n        document.getElementById('viewHeatmapBtn').className = 'px-3 py-1.5 rounded-md font-medium text-slate-400 hover:text-white transition flex items-center gap-1.5';\r\n        document.getElementById('heatmapLegendBar').classList.add('hidden');\r\n        renderScene();\r\n      });\r\n\r\n      safeAddListener('viewHeatmapBtn', 'click', () => {\r\n        viewMode = 'heatmap';\r\n        document.getElementById('viewHeatmapBtn').className = 'px-3 py-1.5 rounded-md font-medium transition flex items-center gap-1.5 bg-amber-600 text-white shadow-sm';\r\n        document.getElementById('viewRealisticBtn').className = 'px-3 py-1.5 rounded-md font-medium text-slate-400 hover:text-white transition flex items-center gap-1.5';\r\n        document.getElementById('heatmapLegendBar').classList.remove('hidden');\r\n        renderScene();\r\n      });\r\n\r\n      safeAddListener('toggleIsoluxBtn', 'click', () => {\r\n        showIsolux = !showIsolux;\r\n        const btn = document.getElementById('toggleIsoluxBtn');\r\n        if (btn) btn.className = showIsolux\r\n          ? 'px-2.5 py-1.5 rounded-lg bg-amber-500\/20 text-amber-300 border border-amber-500\/40 transition flex items-center gap-1.5 font-medium'\r\n          : 'px-2.5 py-1.5 rounded-lg bg-slate-900 text-slate-400 border border-slate-800 transition flex items-center gap-1.5';\r\n        renderScene();\r\n      });\r\n\r\n      safeAddListener('toggleBrickTextureBtn', 'click', () => {\r\n        showBrickTexture = !showBrickTexture;\r\n        const btn = document.getElementById('toggleBrickTextureBtn');\r\n        if (btn) btn.classList.toggle('text-amber-400', showBrickTexture);\r\n        renderScene();\r\n      });\r\n\r\n      safeAddListener('toggleGridBtn', 'click', () => {\r\n        showGrid = !showGrid;\r\n        const btn = document.getElementById('toggleGridBtn');\r\n        if (btn) btn.classList.toggle('text-amber-400', showGrid);\r\n        renderScene();\r\n      });\r\n\r\n      safeAddListener('zoomInBtn', 'click', () => setZoomLevel(viewZoom * 1.25));\r\n      safeAddListener('zoomOutBtn', 'click', () => setZoomLevel(viewZoom * 0.8));\r\n      safeAddListener('zoomResetBtn', 'click', resetZoomPan);\r\n\r\n      \/\/ Presets\r\n      safeAddListener('presetOpticSelect', 'change', (e) => {\r\n        const val = e.target.value;\r\n        if (val === 'graze_tall') {\r\n          setWattage(36);\r\n          setBeam('10x30');\r\n          currentSetback = 0.25;\r\n          currentTilt = 8;\r\n        } else if (val === 'narrow_pencil') {\r\n          setWattage(24);\r\n          setBeam('8');\r\n          currentSetback = 0.15;\r\n          currentTilt = 5;\r\n        } else if (val === 'even_wash') {\r\n          setWattage(48);\r\n          setBeam('15x65');\r\n          currentSetback = 0.45;\r\n          currentTilt = 12;\r\n        } else if (val === 'heavy_flood') {\r\n          setWattage(60);\r\n          setBeam('45');\r\n          currentSetback = 0.60;\r\n          currentTilt = 15;\r\n        }\r\n        document.getElementById('inputSetback').value = currentSetback;\r\n        document.getElementById('valSetback').textContent = `${currentSetback.toFixed(2)} m`;\r\n        document.getElementById('inputTilt').value = currentTilt;\r\n        document.getElementById('valTilt').textContent = `${currentTilt}\u00b0`;\r\n        renderScene();\r\n        updateMetricsUI();\r\n      });\r\n\r\n      \/\/ Tabs\r\n      safeAddListener('tabControlsBtn', 'click', () => {\r\n        document.getElementById('tabControlsBtn').className = 'flex-1 py-2 font-medium rounded-md text-amber-300 bg-slate-900 flex items-center justify-center gap-2 shadow-sm border border-slate-800';\r\n        document.getElementById('tabMetricsBtn').className = 'flex-1 py-2 font-medium rounded-md text-slate-400 hover:text-white flex items-center justify-center gap-2';\r\n        document.getElementById('tabContentControls').classList.remove('hidden');\r\n        document.getElementById('tabContentMetrics').classList.add('hidden');\r\n      });\r\n\r\n      safeAddListener('tabMetricsBtn', 'click', () => {\r\n        document.getElementById('tabMetricsBtn').className = 'flex-1 py-2 font-medium rounded-md text-amber-300 bg-slate-900 flex items-center justify-center gap-2 shadow-sm border border-slate-800';\r\n        document.getElementById('tabControlsBtn').className = 'flex-1 py-2 font-medium rounded-md text-slate-400 hover:text-white flex items-center justify-center gap-2';\r\n        document.getElementById('tabContentMetrics').classList.remove('hidden');\r\n        document.getElementById('tabContentControls').classList.add('hidden');\r\n        updateMetricsUI();\r\n      });\r\n\r\n      safeAddListener('exportPdfBtn', 'click', exportPdfHandler);\r\n    }\r\n\r\n    async function exportPdfHandler() {\r\n      const btn = document.getElementById('exportPdfBtn');\r\n      if (!btn) return;\r\n      const originalHTML = btn.innerHTML;\r\n      btn.innerHTML = `<i class=\"fa-solid fa-spinner fa-spin\"><\/i> Generating PDF...`;\r\n      btn.disabled = true;\r\n\r\n      try {\r\n        const { jsPDF } = window.jspdf || {};\r\n        if (!jsPDF) return;\r\n\r\n        const dateEl = document.getElementById('reportDate');\r\n        if (dateEl) {\r\n          dateEl.textContent = new Date().toLocaleDateString('en-US', {\r\n            year: 'numeric', month: 'short', day: 'numeric'\r\n          });\r\n        }\r\n\r\n        \/\/ Clean snapshot render\r\n        const exportCanvas = document.createElement('canvas');\r\n        exportCanvas.width = CANVAS_WIDTH;\r\n        exportCanvas.height = CANVAS_HEIGHT;\r\n        const expCtx = exportCanvas.getContext('2d');\r\n        const origCtx = ctx;\r\n        const origPan = { ...panOffset };\r\n        const origZoom = viewZoom;\r\n\r\n        ctx = expCtx;\r\n        panOffset = { x: 0, y: 0 };\r\n        viewZoom = 1.0;\r\n        renderScene();\r\n\r\n        const imgContainer = document.getElementById('pdfCanvasImageContainer');\r\n        if (imgContainer) {\r\n          imgContainer.innerHTML = '';\r\n          const img = document.createElement('img');\r\n          img.src = exportCanvas.toDataURL('image\/png');\r\n          img.className = 'w-full h-auto object-contain';\r\n          imgContainer.appendChild(img);\r\n        }\r\n\r\n        \/\/ Restore context\r\n        ctx = origCtx;\r\n        panOffset = origPan;\r\n        viewZoom = origZoom;\r\n        renderScene();\r\n\r\n        \/\/ Populate Table\r\n        const gradTbody = document.getElementById('pdfGradientTableBody');\r\n        if (gradTbody) {\r\n          gradTbody.innerHTML = '';\r\n          const gradient = calculateFacadeMetrics();\r\n          gradient.forEach(g => {\r\n            const tr = document.createElement('tr');\r\n            tr.className = 'border-b border-slate-800';\r\n            tr.innerHTML = `\r\n              <td class=\"p-2 font-medium text-white border-r border-slate-800\">${g.height}<\/td>\r\n              <td class=\"p-2 border-r border-slate-800 font-mono font-bold text-amber-300\">${g.avg} lx<\/td>\r\n              <td class=\"p-2 border-r border-slate-800 font-mono text-slate-300\">${g.min} lx<\/td>\r\n              <td class=\"p-2 border-r border-slate-800 font-mono text-slate-300\">${g.max} lx<\/td>\r\n              <td class=\"p-2 border-r border-slate-800 font-mono text-slate-300\">${g.u0}<\/td>\r\n              <td class=\"p-2 font-medium ${g.avg > 40 ? 'text-emerald-400' : 'text-slate-400'}\">\r\n                ${g.avg > 150 ? 'Intense Ground Graze' : (g.avg > 30 ? 'Defined Masonry Relief' : 'Subtle Upper Wash')}\r\n              <\/td>\r\n            `;\r\n            gradTbody.appendChild(tr);\r\n          });\r\n        }\r\n\r\n        const lumTbody = document.getElementById('pdfLuminaireTableBody');\r\n        if (lumTbody) {\r\n          lumTbody.innerHTML = `\r\n            <tr class=\"border-b border-slate-800\">\r\n              <td class=\"p-2 font-medium text-white border-r border-slate-800\">Architectural Linear Grazer<\/td>\r\n              <td class=\"p-2 border-r border-slate-800 font-mono text-amber-300\">${currentWattage} W \/ meter<\/td>\r\n              <td class=\"p-2 border-r border-slate-800 font-mono text-slate-300\">${currentBeam.includes('x') ? currentBeam.replace('x', '\u00b0 \u00d7 ') + '\u00b0' : currentBeam + '\u00b0'}<\/td>\r\n              <td class=\"p-2 border-r border-slate-800 font-mono text-slate-300\">${currentSetback.toFixed(2)} m<\/td>\r\n              <td class=\"p-2 border-r border-slate-800 font-mono text-slate-300\">${currentTilt}\u00b0<\/td>\r\n              <td class=\"p-2 border-r border-slate-800 font-mono text-slate-300\">${currentCCT}K<\/td>\r\n              <td class=\"p-2 font-mono font-bold text-emerald-400\">${(currentWattage * moduleCount * currentEfficacy).toLocaleString()} lm (${moduleCount}m run)<\/td>\r\n            <\/tr>\r\n          `;\r\n        }\r\n\r\n        const exportContainer = document.getElementById('pdfExportContainer');\r\n        if (exportContainer) {\r\n          exportContainer.classList.remove('hidden');\r\n          const renderedCanvas = await html2canvas(exportContainer, {\r\n            scale: 2,\r\n            useCORS: true,\r\n            backgroundColor: '#020617'\r\n          });\r\n          exportContainer.classList.add('hidden');\r\n\r\n          const pdf = new jsPDF('p', 'mm', 'a4');\r\n          const imgData = renderedCanvas.toDataURL('image\/jpeg', 0.95);\r\n          const pdfWidth = pdf.internal.pageSize.getWidth();\r\n          const pdfHeight = (renderedCanvas.height * pdfWidth) \/ renderedCanvas.width;\r\n\r\n          pdf.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight);\r\n          pdf.save(`Linear_Wall_Washer_Report_${Date.now()}.pdf`);\r\n        }\r\n\r\n      } catch (err) {\r\n        console.error('PDF error:', err);\r\n      } finally {\r\n        btn.innerHTML = originalHTML;\r\n        btn.disabled = false;\r\n      }\r\n    }\r\n\r\n    function init() {\r\n      if (!ensureCanvasContext()) {\r\n        setTimeout(init, 50);\r\n        return;\r\n      }\r\n      updateFixtureArray();\r\n      setupEventListeners();\r\n      renderScene();\r\n      updateMetricsUI();\r\n    }\r\n\r\n    if (document.readyState === 'loading') {\r\n      document.addEventListener('DOMContentLoaded', init);\r\n    } else {\r\n      init();\r\n    }\r\n  <\/script>\r\n<\/body>\r\n<\/html>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>","protected":false},"excerpt":{"rendered":"<p>Real-Time Linear Wall Washer &amp; Grazer Simulator Stop guessing optical reach on high-rise facades. Simulate vertical illuminance ($E_v$) on textured architectural surfaces from $10text{ m}$ heights down to base level. Test asymmetric grazing optics ($10^circ times 30^circ$, $15^circ times 65^circ$), fine-tune setback distances, and analyze live DIALux false-color heatmaps\u2014all inside your browser. I built this [&hellip;]<\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"_acf_changed":false,"footnotes":""},"class_list":["post-15404","page","type-page","status-publish","hentry"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v27.6 (Yoast SEO v28.5) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Real-Time Linear Wall Washer &amp; Grazer Simulator &amp; Lux Calculator Online - Hondel Lighting<\/title>\n<meta name=\"description\" content=\"Stop guessing optical reach on high-rise facades. Simulate vertical illuminance on textured architectural surfaces from $10text{ m}$ heights down to base level. Test asymmetric grazing optics , fine-tune setback distances, and analyze live DIALux false-color heatmaps\u2014all inside your browser.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/hondellighting.com\/ar\/real-time-linear-wall-washer-grazer-simulator.html\/\" \/>\n<meta property=\"og:locale\" content=\"ar_AR\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Real-Time Linear Wall Washer &amp; Grazer Simulator\" \/>\n<meta property=\"og:description\" content=\"Stop guessing optical reach on high-rise facades. Simulate vertical illuminance on textured architectural surfaces from $10text{ m}$ heights down to base level. Test asymmetric grazing optics , fine-tune setback distances, and analyze live DIALux false-color heatmaps\u2014all inside your browser.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/hondellighting.com\/ar\/real-time-linear-wall-washer-grazer-simulator.html\/\" \/>\n<meta property=\"og:site_name\" content=\"Hondel Lighting Limited\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-17T07:50:56+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"\u0648\u0642\u062a \u0627\u0644\u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0645\u064f\u0642\u062f\u0651\u0631\" \/>\n\t<meta name=\"twitter:data1\" content=\"\u062f\u0642\u064a\u0642\u062a\u0627\u0646\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/hondellighting.com\\\/real-time-linear-wall-washer-grazer-simulator.html\",\"url\":\"https:\\\/\\\/hondellighting.com\\\/real-time-linear-wall-washer-grazer-simulator.html\",\"name\":\"Real-Time Linear Wall Washer & Grazer Simulator & Lux Calculator Online - Hondel Lighting\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/hondellighting.com\\\/#website\"},\"datePublished\":\"2026-10-17T07:29:12+00:00\",\"dateModified\":\"2026-10-17T07:50:56+00:00\",\"description\":\"Stop guessing optical reach on high-rise facades. Simulate vertical illuminance on textured architectural surfaces from $10\\\\text{ m}$ heights down to base level. Test asymmetric grazing optics , fine-tune setback distances, and analyze live DIALux false-color heatmaps\u2014all inside your browser.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/hondellighting.com\\\/real-time-linear-wall-washer-grazer-simulator.html#breadcrumb\"},\"inLanguage\":\"ar\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/hondellighting.com\\\/real-time-linear-wall-washer-grazer-simulator.html\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/hondellighting.com\\\/real-time-linear-wall-washer-grazer-simulator.html#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/hondellighting.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Real-Time Linear Wall Washer &#038; Grazer Simulator\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/hondellighting.com\\\/#website\",\"url\":\"https:\\\/\\\/hondellighting.com\\\/\",\"name\":\"Hondel Lighting\",\"description\":\"Facade Lighting Manufacturer\",\"publisher\":{\"@id\":\"https:\\\/\\\/hondellighting.com\\\/#organization\"},\"alternateName\":\"Hondel\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/hondellighting.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"ar\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/hondellighting.com\\\/#organization\",\"name\":\"Hondel Lighting Limited\",\"url\":\"https:\\\/\\\/hondellighting.com\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"ar\",\"@id\":\"https:\\\/\\\/hondellighting.com\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/hondellighting.com\\\/wp-content\\\/uploads\\\/2023\\\/10\\\/hondel-logo-black-png.png\",\"contentUrl\":\"https:\\\/\\\/hondellighting.com\\\/wp-content\\\/uploads\\\/2023\\\/10\\\/hondel-logo-black-png.png\",\"width\":86,\"height\":40,\"caption\":\"Hondel Lighting Limited\"},\"image\":{\"@id\":\"https:\\\/\\\/hondellighting.com\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Real-Time Linear Wall Washer & Grazer Simulator & Lux Calculator Online - Hondel Lighting","description":"Stop guessing optical reach on high-rise facades. Simulate vertical illuminance on textured architectural surfaces from $10text{ m}$ heights down to base level. Test asymmetric grazing optics , fine-tune setback distances, and analyze live DIALux false-color heatmaps\u2014all inside your browser.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/hondellighting.com\/ar\/real-time-linear-wall-washer-grazer-simulator.html\/","og_locale":"ar_AR","og_type":"article","og_title":"Real-Time Linear Wall Washer & Grazer Simulator","og_description":"Stop guessing optical reach on high-rise facades. Simulate vertical illuminance on textured architectural surfaces from $10text{ m}$ heights down to base level. Test asymmetric grazing optics , fine-tune setback distances, and analyze live DIALux false-color heatmaps\u2014all inside your browser.","og_url":"https:\/\/hondellighting.com\/ar\/real-time-linear-wall-washer-grazer-simulator.html\/","og_site_name":"Hondel Lighting Limited","article_modified_time":"2026-10-17T07:50:56+00:00","twitter_card":"summary_large_image","twitter_misc":{"\u0648\u0642\u062a \u0627\u0644\u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0645\u064f\u0642\u062f\u0651\u0631":"\u062f\u0642\u064a\u0642\u062a\u0627\u0646"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/hondellighting.com\/real-time-linear-wall-washer-grazer-simulator.html","url":"https:\/\/hondellighting.com\/real-time-linear-wall-washer-grazer-simulator.html","name":"Real-Time Linear Wall Washer & Grazer Simulator & Lux Calculator Online - Hondel Lighting","isPartOf":{"@id":"https:\/\/hondellighting.com\/#website"},"datePublished":"2026-10-17T07:29:12+00:00","dateModified":"2026-10-17T07:50:56+00:00","description":"Stop guessing optical reach on high-rise facades. Simulate vertical illuminance on textured architectural surfaces from $10\\text{ m}$ heights down to base level. Test asymmetric grazing optics , fine-tune setback distances, and analyze live DIALux false-color heatmaps\u2014all inside your browser.","breadcrumb":{"@id":"https:\/\/hondellighting.com\/real-time-linear-wall-washer-grazer-simulator.html#breadcrumb"},"inLanguage":"ar","potentialAction":[{"@type":"ReadAction","target":["https:\/\/hondellighting.com\/real-time-linear-wall-washer-grazer-simulator.html"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/hondellighting.com\/real-time-linear-wall-washer-grazer-simulator.html#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/hondellighting.com\/"},{"@type":"ListItem","position":2,"name":"Real-Time Linear Wall Washer &#038; Grazer Simulator"}]},{"@type":"WebSite","@id":"https:\/\/hondellighting.com\/#website","url":"https:\/\/hondellighting.com\/","name":"Hondel Lighting","description":"Facade Lighting Manufacturer","publisher":{"@id":"https:\/\/hondellighting.com\/#organization"},"alternateName":"Hondel","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/hondellighting.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"ar"},{"@type":"Organization","@id":"https:\/\/hondellighting.com\/#organization","name":"Hondel Lighting Limited","url":"https:\/\/hondellighting.com\/","logo":{"@type":"ImageObject","inLanguage":"ar","@id":"https:\/\/hondellighting.com\/#\/schema\/logo\/image\/","url":"https:\/\/hondellighting.com\/wp-content\/uploads\/2023\/10\/hondel-logo-black-png.png","contentUrl":"https:\/\/hondellighting.com\/wp-content\/uploads\/2023\/10\/hondel-logo-black-png.png","width":86,"height":40,"caption":"Hondel Lighting Limited"},"image":{"@id":"https:\/\/hondellighting.com\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/hondellighting.com\/ar\/wp-json\/wp\/v2\/pages\/15404","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hondellighting.com\/ar\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/hondellighting.com\/ar\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/hondellighting.com\/ar\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hondellighting.com\/ar\/wp-json\/wp\/v2\/comments?post=15404"}],"version-history":[{"count":13,"href":"https:\/\/hondellighting.com\/ar\/wp-json\/wp\/v2\/pages\/15404\/revisions"}],"predecessor-version":[{"id":15418,"href":"https:\/\/hondellighting.com\/ar\/wp-json\/wp\/v2\/pages\/15404\/revisions\/15418"}],"wp:attachment":[{"href":"https:\/\/hondellighting.com\/ar\/wp-json\/wp\/v2\/media?parent=15404"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}