<!DOCTYPE html> <html lang="en"> <head> <title>Protoserve</title> <link rel="icon" type="image/png" href="static/favicon-512.png"> <link rel="apple-touch-icon" href="static/favicon-512.png"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> :root { --u-display-metric: default; --u-display-us: none; } html, body { margin: 0; width: 100%; height: 100%; font-family: Helvetica, Segoe UI, Sans-Serif; } body { display: grid; grid-template-columns: clamp(200px, 500px, 50vw) 1fr; grid-template-rows: 1fr 0fr; grid-template-areas: "controls main" "links main"; } #controls { grid-area: controls; user-select: none; display: grid; grid-template-columns: 10fr 1fr 1fr; align-content: start; overflow-y: scroll; } label { grid-column-start: 1; grid-column-end: span 3; display: grid; grid-template-columns: subgrid; text-align: right; } input[type="checkbox"] { justify-self: start; } input { align-self: start; } .group > h4, .group > h5 { text-align: center; margin: 5px; grid-column-start: 1; grid-column-end: span 3; } .expand > :first-child { grid-column-start: 1; grid-column-end: span 3; } .group, .field, .expand { grid-column-start: 1; grid-column-end: span 3; display: grid; grid-template-columns: subgrid; align-content: start; } .group { background-color: hsl(.0turn 50% 10% / 4%); padding: 10px; border-radius: 10px; box-shadow: 0 0 8px 0px hsl(0 0% 0% / 20%); margin: 10px 0 10px 0; } .expand > .field:first-child { display: grid; grid-template-columns: 1fr 100fr 1fr; align-items: center; text-align: left; width: 100%; position: relative; } .group > .content { grid-column-start: 1; grid-column-end: span 3; display: flex; flex-direction: column; text-align: center; } .group > .attribution, .group > .usage { grid-column-start: 1; grid-column-end: span 3; } .group > div > .proportion { display: none; } .proportional > div > .proportion { display: grid; } .split-sides .double-sided-only { display: none; } .split-sides > .placeholder .area-controls { display: none; } .board > .placeholder > .area-controls { display: none; } .area-controls .area-move { display: none; } .area-controls .area-move::before { content: "/"; padding: 0 5px 0 5px; } .group.proportional > .group > .area-controls .area-move { display: block; } .content.area-controls { flex-direction: row; justify-content: center; } .field > input, .field > select { max-width: 5em; text-align: right; margin: 0 5px 0 5px; } input[type="text"]:invalid { background: rgba(255 0 0 / 30%); } input[type="text"]:focus:valid { background: rgba(0 192 64 / 30%); } .group.expand { border-radius: 0; } .expand > :first-child { font-weight: bold; } .expand.collapsed > :nth-child(n+2) { display: none; } .unit.metric { display: var(--u-display-metric); } .unit.us { display: var(--u-display-us); } #preview { position: relative; grid-area: main; padding: 20px; } #preview-image { width: 100%; height: 100%; object-fit: contain; } #preview-message { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: hsla(0 0% 50% / 30%); display: none; justify-content: center; align-items: center; font-size: 18pt; font-weight: bold; color: white; } #preview-message.loading { display: flex; } #links { grid-area: links; display: flex; justify-content: center; padding: 5px; } #link-gerbers { background-color: #0d6efd; color: white; font-weight: bold; box-shadow: 0px 0px 1px 1px hsl(0, 0%, 0% / 20%); border-radius: .5em; padding: 1em 2em 1em 2em; } .layout-area { grid-column-start: 1; grid-column-end: span 3; } .drop-target { grid-column-start: 1; grid-column-end: span 3; text-align: center; display: none; } .group.drop-enabled > .drop-target { display: block; } .placeholder hr { width: 3em; border: none; border-top: 1px solid hsl(0 0% 60%); } #controls.move-in-progress input { background-color: hsl(0 0% 85%); } #controls.move-in-progress { color: hsl(0 0% 60%); } .narrow-only { display: none; } </style> </head> <body> <div id="controls"> <form> <div class="group board"> <h4>Board settings</h4> <label>Units <select name='units' value="metric"> <option value="metric">Metric</option> <option value="us">US Customary</option> </select> </label> <label>Board width <input name="width" type="text" placeholder="width" value="100" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">inch</span> </label> <label>Board height <input name="height" type="text" placeholder="height" value="80" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">inch</span> </label> <div class="group expand" data-group="round_corners"> <label>Round corners <input name="enabled" type="checkbox" checked/> </label> <label>Radius <input name="radius" type="text" placeholder="radius" value="1.5" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">inch</span> </label> </div> <div class="group expand" data-group="mounting_holes"> <label>Mounting holes <input name="enabled" type="checkbox" name="has_holes" checked/> </label> <label>Diameter <input type="text" placeholder="diameter" name="diameter" value="3.2" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">inch</span> </label> <label>Board edge to hole center <input type="text" placeholder="distance" name="offset" value="5" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">inch</span> </label> </div> <h4>Content</h4> <div class="group placeholder"></div> </div> </form> </div> <div id="preview"> <img id="preview-image" alt="Automatically generated preview image"/> <div id="preview-message"></div> </div> <div id="links"> <a class="narrow-only" href="#controls">Settings</a> <a class="narrow-only" href="#preview">Preview</a> <a id="link-gerbers" href='#'> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em"> <title>Download</title> <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32V274.7l-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7V32zM64 352c-35.3 0-64 28.7-64 64v32c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V416c0-35.3-28.7-64-64-64H346.5l-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352H64zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/> </svg> Gerbers </a> </div> <template id="tpl-drop-target"> <a class="drop-target" href="#"> <svg viewBox="0 0 532 532" width="1em" xmlns="http://www.w3.org/2000/svg"> <title>Move here</title> <path id="path2" d="m 424.025,300.075 c 17.7,0 32,-14.3 32,-32 0,-17.7 -14.3,-32 -32,-32 h -82.7 l 181.3,-181.4 c 12.5,-12.5 12.5,-32.8 0,-45.3 -12.5,-12.5 -32.8,-12.5 -45.3,0 l -181.3,181.4 v -82.7 c 0,-17.7 -14.3,-32 -32,-32 -17.7,0 -32,14.3 -32,32 v 160 c 0,17.7 14.3,32 32,32 z M 80,52 C 35.8,52 0,87.8 0,132 v 320 c 0,44.2 35.8,80 80,80 h 320 c 44.2,0 80,-35.8 80,-80 v -72 c 0,-17.7 -14.3,-32 -32,-32 -17.7,0 -32,14.3 -32,32 v 72 c 0,8.8 -7.2,16 -16,16 H 80 c -8.8,0 -16,-7.2 -16,-16 V 132 c 0,-8.8 7.2,-16 16,-16 h 72 c 17.7,0 32,-14.3 32,-32 0,-17.7 -14.3,-32 -32,-32 z" /> </svg> </a> </template> <template id="tpl-g-layout"> <div data-type="layout" class="group proportional"> <h4>Proportional Layout</h4> <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span> <label class="proportion">Proportion <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/> </label> <h5>Layout settings</h4> <label>Direction <select name="direction" value="horizontal"> <option value="h">horizontal</option> <option value="v">vertical</option> </select> </label> <h5>Content</h4> <div class="drop-target"></div> <div class="placeholder"></div> <div class="drop-target"></div> <div class="placeholder"></div> <div class="drop-target"></div> <a class="content add-element" href="#">Add element</a> </div> </template> <template id="tpl-g-twoside"> <div data-type='twoside' class="group split-sides"> <h4>Split front and back</h4> <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span> <label class="proportion">Proportion <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/> </label> <h5>Front</h5> <div class="placeholder"></div> <h5>Back</h5> <div class="placeholder"></div> </div> </template> <template id="tpl-g-placeholder"> <div data-type="placeholder" class="group placeholder"> <h4>Empty area</h4> <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span> <label class="proportion">Proportion <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/> </label> <div class="content"> <a href="#" data-placeholder="layout">Create Layout</a> <a href="#" data-placeholder="twoside" class="double-sided-only">Split front and back</a> <hr/> <a href="#" data-placeholder="smd">SMD area</a> <a href="#" data-placeholder="tht" class="double-sided-only">THT area</a> <a href="#" data-placeholder="manhattan">Manhattan area</a> <a href="#" data-placeholder="flower" class="double-sided-only">THT Flower area</a> <a href="#" data-placeholder="powered" class="double-sided-only">Powered THT area</a> <a href="#" data-placeholder="rf" class="double-sided-only">RF THT area</a> <a href="#" data-placeholder="spiky" class="double-sided-only">Spiky hybrid area</a> <a href="#" data-placeholder="alio" class="double-sided-only">ALio hybrid area</a> </div> </div> </template> <template id="tpl-g-smd"> <div data-type="smd" class="group smd"> <h4>SMD area</h4> <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span> <label class="proportion">Proportion <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/> </label> <h5>Area Settings</h5> <label>Pitch X <input type="text" name="pitch_x" placeholder="length" value="1.27" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Pitch Y <input type="text" name="pitch_y" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Clearance <input type="text" name="clearance" placeholder="length" value="0.3" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Pad shape <select name="pad_shape" value="rect"> <option value="rect">(Rounded) Rectangle</option> <option value="circle">Circle</option> </select> </label> <label class="only-shape rect">Corner radius <input type="text" name="pad_h" placeholder="length" value="0" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> </div> </template> <template id="tpl-g-tht"> <div data-type="tht" class="group tht"> <h4>THT area</h4> <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span> <label class="proportion">Proportion <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/> </label> <h5>Area Settings</h5> <label>Pitch X <input type="text" name="pitch_x" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Pitch Y <input type="text" name="pitch_y" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Clearance <input type="text" name="clearance" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Plating <select name="plating" value="through"> <option value="plated">Double-sided, through-plated</option> <option value="nonplated">Double-sided, non-plated</option> <option value="singleside">Single-sided, non-plated</option> </select> </label> <label>Hole diameter <input type="text" name="hole_dia" placeholder="length" value="0.9" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Pad shape <select name="pad_shape" value="circle"> <option value="circle">Circle</option> <option value="rect">(Rounded) Rectangle</option> <option value="obround">Obround</option> </select> </label> <label class="only-shape rect">Corner radius <input type="text" name="pad_h" placeholder="length" value="0" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> </div> </template> <template id="tpl-g-manhattan"> <div data-type="manhattan" class="group manhattan"> <h4>Manhattan area</h4> <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span> <label class="proportion">Proportion <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/> </label> <h5>Area Settings</h5> <label>Pitch X <input type="text" name="pitch_x" placeholder="length" value="5.08" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Pitch Y <input type="text" name="pitch_y" placeholder="length" value="5.08" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Clearance <input type="text" name="clearance" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> </div> </template> <template id="tpl-g-flower"> <div data-type="flower" class="group flower"> <h4>THT flower area</h4> <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span> <label class="proportion">Proportion <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/> </label> <h5>Area Settings</h5> <label>Pitch <input type="text" name="pitch" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Pattern diameter <input type="text" name="pattern_dia" placeholder="length" value="2.0" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Hole diameter <input type="text" name="hole_dia" placeholder="length" value="0.9" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Clearance <input type="text" name="clearance" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> </div> </template> <template id="tpl-g-powered"> <div data-type="powered" class="group powered"> <h4>Powered THT area</h4> <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span> <label class="proportion">Proportion <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/> </label> <h5>Area Settings</h5> <label>Pitch <input type="text" name="pitch" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Hole diameter <input type="text" name="hole_dia" placeholder="length" value="0.9" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Via drill <input type="text" name="via_hole_dia" placeholder="length" value="0.4" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Via diameter <input type="text" name="via_dia" placeholder="length" value="1.1" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Trace width <input type="text" name="trace_width" placeholder="length" value="0.4" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Clearance <input type="text" name="clearance" placeholder="length" value="0.2" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> </div> </template> <template id="tpl-g-rf"> <div data-type="rf" class="group rf"> <h4>THT area with RF ground</h4> <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span> <label class="proportion">Proportion <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/> </label> <h5>Area Settings</h5> <label>Pitch <input type="text" name="pitch" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Hole diameter <input type="text" name="hole_dia" placeholder="length" value="0.9" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Trace width <input type="text" name="trace_width" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Via diameter <input type="text" name="via_dia" placeholder="length" value="0.8" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Via drill <input type="text" name="via_hole_dia" placeholder="length" value="0.4" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Clearance <input type="text" name="clearance" placeholder="length" value="0.3" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> </div> </template> <template id="tpl-g-spiky"> <div data-type="spiky" class="group spiky"> <h4>Spiky hybrid area</h4> <div class="attribution"> Layout by <a href="https://social.treehouse.systems/@electronic_eel">electroniceel</a> (<a href="https://github.com/electroniceel/protoboard">github</a>) </div> <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span> <label class="proportion">Proportion <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/> </label> <h5>Area Settings</h5> <div class="usage">This area has a fixed 100 mil / 2.54 mm pitch.</div> </div> </template> <template id="tpl-g-alio"> <div data-type="alio" class="group alio"> <h4>ALio hybrid area</h4> <div class="attribution"> Layout by arief ibrahim adha (<a href="https://hackaday.io/project/28570-alio-new-hardware-prototyping-platform">hackaday.io</a>). Top and bottom have opposed orientation of the SMD pads. </div> <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span> <label class="proportion">Proportion <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/> </label> <h5>Area Settings</h5> <label>Pitch <input type="text" name="pitch" placeholder="length" value="2.54" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Hole diameter <input type="text" name="hole_dia" placeholder="length" value="0.9" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Link trace width <input type="text" name="link_trace_width" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Via pad width <input type="text" name="link_pad_width" placeholder="length" value="0.8" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Via drill <input type="text" name="via_hole_dia" placeholder="length" value="0.4" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> <label>Clearance <input type="text" name="clearance" placeholder="length" value="0.3" pattern="[0-9]+\.?[0-9]*"/> <span class="unit metric">mm</span> <span class="unit us">mil</span> </label> </div> </template> <script> document.querySelectorAll('.expand').forEach((elem) => { const checkbox = elem.querySelector(':first-child > input'); checkbox.addEventListener("change", (evt) => { if (evt.currentTarget.checked) { elem.classList.remove('collapsed'); } else { elem.classList.add('collapsed'); } }); if (checkbox.checked) { elem.classList.remove('collapsed'); } else { elem.classList.add('collapsed'); } }); let g_dropElement = null; function hookupAreaRemove(node) { for (const bt of node.querySelectorAll('a.area-remove')) { bt.addEventListener('click', (evt) => { let elem = evt.target.closest('.group'); if (elem.parentElement && elem.parentElement.matches('.proportional')) { let sibling = elem.previousElementSibling; if (sibling.matches('.drop-target')) { sibling.remove(); } elem.remove(); } else { elem.replaceWith(createPlaceholder()); } previewReloader.scheduleCall(); }); } } function createDropTarget() { const node = document.querySelector('#tpl-drop-target').content.cloneNode(true); node.querySelector('a').addEventListener('click', (evt) => { if (g_dropElement != null) { const target = evt.target.closest('a'); let sibling = g_dropElement.previousElementSibling; if (sibling.matches('.drop-target')) { if (sibling == target) { return; } sibling.remove(); } g_dropElement.remove(); g_dropElement.querySelector('a.area-move').innerText = "Move"; target.before(sibling); target.before(g_dropElement); document.querySelector('#controls').classList.remove('move-in-progress'); document.querySelector('.group.drop-enabled').classList.remove('drop-enabled'); g_dropElement = null; previewReloader.scheduleCall(); } }); return node; } function hookupAreaMove(node) { for (const bt of node.querySelectorAll('a.area-move')) { bt.addEventListener('click', (evt) => { const controls = document.querySelector('#controls'); const group = evt.target.closest('.group'); if (g_dropElement == null) { controls.classList.add('move-in-progress'); group.parentElement.classList.add('drop-enabled'); g_dropElement = group; evt.target.innerText = "Cancel move"; } else { controls.classList.remove('move-in-progress'); group.parentElement.classList.remove('drop-enabled'); g_dropElement = null; evt.target.innerText = "Move"; } }); } } function hookupPreviewUpdate(node) { for (const elem of node.querySelectorAll('select, input')) { elem.addEventListener('change', previewReloader.scheduleCall.bind(previewReloader)); } } function createLayoutItem(type) { if (type == 'placeholder') { return createPlaceholder(); } const template = document.querySelector(`#tpl-g-${type}`); const node = template.content.cloneNode(true).firstElementChild; hookupPreviewUpdate(node); hookupAreaRemove(node); hookupAreaMove(node); for (const bt of node.querySelectorAll(':scope > a.add-element')) { bt.addEventListener('click', (evt) => { evt.target.before(createPlaceholder()); evt.target.before(createDropTarget()); previewReloader.scheduleCall(); }); } function updateShapeFilter(filterNode) { console.log(filterNode); for (elem of filterNode.closest('.group').querySelectorAll('.only-shape')) { if (elem.classList.contains(filterNode.value)) { elem.style.removeProperty('display'); } else { elem.style.setProperty('display', 'none'); } } } if (type == 'tht' || type == 'smd') { const filterNode = node.querySelector('select[name="pad_shape"]'); updateShapeFilter(filterNode); filterNode.addEventListener('change', (evt) => { updateShapeFilter(evt.target); }); } return node; } function createPlaceholder() { const node = document.querySelector('#tpl-g-placeholder').content.cloneNode(true).firstElementChild; hookupAreaRemove(node); hookupAreaMove(node); for (const bt of node.querySelectorAll('.placeholder a[data-placeholder]')) { bt.addEventListener('click', (evt) => { const item = createLayoutItem(evt.target.getAttribute('data-placeholder')); for (const elem of item.querySelectorAll('div.placeholder')) { elem.replaceWith(createPlaceholder()); } for (const elem of item.querySelectorAll('div.drop-target')) { elem.replaceWith(createDropTarget()); } evt.target.closest('.group').replaceWith(item); previewReloader.scheduleCall(); }); } return node; } function serializeNode(node) { function serializeProperties(node) { let obj = {}; for (const input of node.querySelectorAll(':scope > label > input, :scope > label > select')) { if (input.type == 'checkbox') { obj[input.name] = input.checked; } else { obj[input.name] = input.value; } } return obj; } const obj = serializeProperties(node); for (const expand of node.querySelectorAll(':scope > .group.expand')) { obj[expand.getAttribute('data-group')] = serializeProperties(expand); } const children = []; for (const elem of node.querySelectorAll(':scope > .group:not(.expand)')) { const child = serializeNode(elem); child['type'] = elem.getAttribute('data-type'); children.push(child); } obj['children'] = children; return obj; } function serialize() { const board = document.querySelector('.group.board'); return JSON.stringify(serializeNode(board)); } function deserializeNode(node, obj) { function deserializeProperties(node, obj) { for (const input of node.querySelectorAll(':scope > label > input, :scope > label > select')) { if (input.type == 'checkbox') { input.checked = obj[input.name]; } else { input.value = obj[input.name]; } } } deserializeProperties(node, obj); for (const expand of node.querySelectorAll(':scope > .group.expand')) { deserializeProperties(expand, obj[expand.getAttribute('data-group')]); } for (const child of obj['children']) { const type = child['type']; if (type) { const item = createLayoutItem(type); deserializeNode(item, child); if (type == 'layout') { for (const elem of item.querySelectorAll('div.placeholder, div.drop-target')) { elem.remove(); } } else { for (const elem of item.querySelectorAll('div.drop-target')) { elem.replaceWith(createDropTarget()); } } if (obj['type'] == 'layout') { const addLink = node.querySelector(':scope > a.add-element'); addLink.before(item); addLink.before(createDropTarget()); } else { const placeholder = node.querySelector('div.placeholder'); placeholder.replaceWith(item); } } } } function deserialize(json) { const board = document.querySelector('.group.board'); const data = JSON.parse(json); deserializeNode(board, data); previewReloader.scheduleCall(); } class RateLimiter { constructor(callback, interval_ms) { this.callback = callback; this.interval_ms = interval_ms; this.lastRan = -1e99; this.timerId = null; } callNow() { const now = performance.timeOrigin + performance.now(); this.lastRan = now; this.timerId = null; this.callback(); } scheduleCall() { const now = performance.timeOrigin + performance.now(); const timeRemaining = this.interval_ms - (now - this.lastRan); console.log('scheduling', timeRemaining); if (!this.timerId) { if (timeRemaining <= 0) { this.callNow(); } else { const callback = this.callback; this.timerId = setTimeout(this.callNow.bind(this), timeRemaining); } } } } let previewBlobURL = null; previewReloader = new RateLimiter(async () => { if (document.querySelector('form').checkValidity()) { document.querySelector('#preview-message').textContent = 'Reloading...'; document.querySelector('#preview-message').classList.add('loading'); const response = await fetch('preview.svg', { method: 'POST', mode: 'same-origin', cache: 'no-cache', headers: {'Content-Type': 'application/json'}, body: serialize(), }); const data = await response.blob(); if (previewBlobURL) { URL.revokeObjectURL(previewBlobURL); } previewBlobURL = URL.createObjectURL(data); document.querySelector('#preview-image').src = previewBlobURL; document.querySelector('#preview-message').textContent = ''; document.querySelector('#preview-message').classList.remove('loading'); } else { document.querySelector('#preview-message').classList.add('loading'); document.querySelector('#preview-message').textContent = 'Please correct any invalid fields.'; } }, 1000); document.querySelector('div.placeholder').replaceWith(createPlaceholder()); for (elem of document.querySelectorAll('select[name="units"]')) { elem.addEventListener('change', (evt) => { const style = evt.target.closest('.group').style; for (const unit of ['metric', 'us']) { const value = (unit == evt.target.value) ? 'default' : 'none'; style.setProperty(`--u-display-${unit}`, value); } }); } let downloadObjectURL = null; document.querySelector('#link-gerbers').addEventListener('click', async () => { const response = await fetch('gerbers.zip', { method: 'POST', mode: 'same-origin', cache: 'no-cache', headers: {'Content-Type': 'application/json'}, body: serialize(), }); const data = await response.blob(); /* cf. https://gist.github.com/devloco/5f779216c988438777b76e7db113d05c */ const zipBlob = new Blob([data], { type: 'application/zip' }); if (downloadObjectURL) { URL.revokeObjectURL(downloadObjectURL); } downloadObjectURL = URL.createObjectURL(zipBlob); let link = document.createElement('a'); link.href = downloadObjectURL; link.download = 'gerbers.zip'; link.click(); }); hookupPreviewUpdate(document.querySelector('.group.board')); previewReloader.scheduleCall(); </script> </body> </html>