diff options
author | jaseg <git@jaseg.de> | 2023-04-09 17:24:50 +0200 |
---|---|---|
committer | jaseg <git@jaseg.de> | 2023-04-09 17:24:50 +0200 |
commit | c9dff5450fa40f49727f5c447081d0df28484b1b (patch) | |
tree | 133eb03e79be88974ed09b0d35680ace9c037800 /gerbonara/cad | |
parent | 3b5fb41ecb9828fd13fe1a849448a3d41c8f1867 (diff) | |
download | gerbonara-c9dff5450fa40f49727f5c447081d0df28484b1b.tar.gz gerbonara-c9dff5450fa40f49727f5c447081d0df28484b1b.tar.bz2 gerbonara-c9dff5450fa40f49727f5c447081d0df28484b1b.zip |
protogen web interface works
Diffstat (limited to 'gerbonara/cad')
-rw-r--r-- | gerbonara/cad/primitives.py | 28 | ||||
-rw-r--r-- | gerbonara/cad/protoboard.py | 17 | ||||
-rw-r--r-- | gerbonara/cad/protoserve.py | 131 | ||||
-rw-r--r-- | gerbonara/cad/protoserve_data/protoserve.html | 899 |
4 files changed, 1055 insertions, 20 deletions
diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py index b566d76..abce1a4 100644 --- a/gerbonara/cad/primitives.py +++ b/gerbonara/cad/primitives.py @@ -342,17 +342,21 @@ class THTPad(Pad): x, y, rotation = self.abs_pos self.pad_top.parent = self self.pad_top.render(layer_stack) - self.pad_bottom.parent = self - self.pad_bottom.render(layer_stack) + if self.pad_bottom: + self.pad_bottom.parent = self + self.pad_bottom.render(layer_stack) if self.aperture_inner is None: (x_min, y_min), (x_max, y_max) = self.pad_top.bounding_box(MM) w_top = x_max - x_min h_top = y_max - y_min - (x_min, y_min), (x_max, y_max) = self.pad_bottom.bounding_box(MM) - w_bottom = x_max - x_min - h_bottom = y_max - y_min - self.aperture_inner = CircleAperture(min(w_top, h_top, w_bottom, h_bottom), unit=MM) + if self.pad_bottom: + (x_min, y_min), (x_max, y_max) = self.pad_bottom.bounding_box(MM) + w_bottom = x_max - x_min + h_bottom = y_max - y_min + w_top = min(w_top, w_bottom) + h_top = min(h_top, h_bottom) + self.aperture_inner = CircleAperture(min(w_top, h_top), unit=MM) for (side, use), layer in layer_stack.inner_layers: layer.objects.append(Flash(x, y, self.aperture_inner.rotated(rotation), unit=self.unit)) @@ -368,24 +372,24 @@ class THTPad(Pad): return False @classmethod - def rect(kls, x, y, hole_dia, w, h=None, rotation=0, mask_expansion=0.0, paste_expansion=0.0, paste=True, unit=MM): + def rect(kls, x, y, hole_dia, w, h=None, rotation=0, mask_expansion=0.0, paste_expansion=0.0, paste=True, plated=True, unit=MM): if h is None: h = w pad = SMDPad.rect(0, 0, w, h, mask_expansion=mask_expansion, paste_expansion=paste_expansion, paste=paste, unit=unit) - return kls(x, y, hole_dia, pad, rotation=rotation, unit=unit) + return kls(x, y, hole_dia, pad, rotation=rotation, plated=plated, unit=unit) @classmethod - def circle(kls, x, y, hole_dia, dia, mask_expansion=0.0, paste_expansion=0.0, paste=True, unit=MM): + def circle(kls, x, y, hole_dia, dia, mask_expansion=0.0, paste_expansion=0.0, paste=True, plated=True, unit=MM): pad = SMDPad.circle(0, 0, dia, mask_expansion=mask_expansion, paste_expansion=paste_expansion, paste=paste, unit=unit) - return kls(x, y, hole_dia, pad, unit=unit) + return kls(x, y, hole_dia, pad, plated=plated, unit=unit) @classmethod - def obround(kls, x, y, hole_dia, w, h, rotation=0, mask_expansion=0.0, paste_expanson=0.0, paste=True, unit=MM): + def obround(kls, x, y, hole_dia, w, h, rotation=0, mask_expansion=0.0, paste_expanson=0.0, paste=True, plated=True, unit=MM): ap_c = CircleAperture(dia, unit=unit) ap_m = CircleAperture(dia+2*mask_expansion, unit=unit) ap_p = CircleAperture(dia+2*paste_expansion, unit=unit) if paste else None pad = SMDPad(0, 0, side='top', copper_aperture=ap_c, mask_aperture=ap_m, paste_aperture=ap_p, unit=unit) - return kls(x, y, hole_dia, pad, rotation=rotation, unit=unit) + return kls(x, y, hole_dia, pad, rotation=rotation, plated=plated, unit=unit) @dataclass diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index 41276e2..a3d616f 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -57,9 +57,9 @@ class PropLayout: first = bool(i == 0) last = bool(i == len(self.content)-1) yield from child.generate(bbox, ( - border_text[0] and (first or self.direction == 'h'), + border_text[0] and (last or self.direction == 'h'), border_text[1] and (last or self.direction == 'v'), - border_text[2] and (last or self.direction == 'h'), + border_text[2] and (first or self.direction == 'h'), border_text[3] and (first or self.direction == 'v'), ), unit) @@ -493,19 +493,20 @@ def eval_value(value, total_length=None): def _demo(): - #pattern1 = PatternProtoArea(2.54, obj=THTPad.circle(0, 0, 0.9, 1.8, paste=False)) + pattern1 = PatternProtoArea(2.54, obj=THTPad.circle(0, 0, 0.9, 1.8, paste=False)) pattern2 = PatternProtoArea(1.2, 2.0, obj=SMDPad.rect(0, 0, 1.0, 1.8, paste=False)) - #pattern3 = PatternProtoArea(2.54, 1.27, obj=SMDPad.rect(0, 0, 2.3, 1.0, paste=False)) - pattern3 = EmptyProtoArea(copper_fill=True) - stack = TwoSideLayout(pattern2, pattern3) - #pattern = PropLayout([pattern1, stack], 'h', [0.5, 0.5]) + pattern3 = PatternProtoArea(2.54, 1.27, obj=SMDPad.rect(0, 0, 2.3, 1.0, paste=False)) + #pattern3 = EmptyProtoArea(copper_fill=True) + #stack = TwoSideLayout(pattern2, pattern3) + stack = PropLayout([pattern2, pattern3], 'v', [0.5, 0.5]) + pattern = PropLayout([pattern1, stack], 'h', [0.5, 0.5]) #pattern = PatternProtoArea(2.54, obj=ManhattanPads(2.54)) #pattern = PatternProtoArea(2.54, obj=PoweredProto()) #pattern = PatternProtoArea(2.54, obj=RFGroundProto()) #pattern = PatternProtoArea(2.54*1.5, obj=THTFlowerProto()) #pattern = PatternProtoArea(2.54, obj=THTPad.circle(0, 0, 0.9, 1.8, paste=False)) #pattern = PatternProtoArea(2.54, obj=PoweredProto()) - pb = ProtoBoard(100, 80, stack, mounting_hole_dia=3.2, mounting_hole_offset=5) + pb = ProtoBoard(100, 80, pattern, mounting_hole_dia=3.2, mounting_hole_offset=5) print(pb.pretty_svg()) pb.layer_stack().save_to_directory('/tmp/testdir') diff --git a/gerbonara/cad/protoserve.py b/gerbonara/cad/protoserve.py new file mode 100644 index 0000000..0bf9dce --- /dev/null +++ b/gerbonara/cad/protoserve.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python + +import importlib.resources +from tempfile import TemporaryDirectory +from pathlib import Path + +from quart import Quart, request, Response + +from . import protoboard as pb +from . import protoserve_data +from ..utils import MM, Inch + + +def extract_importlib(package): + root = TemporaryDirectory() + + stack = [(importlib.resources.files(package), Path(root.name))] + while stack: + res, out = stack.pop() + + for item in res.iterdir(): + item_out = out / item.name + if item.is_file(): + item_out.write_bytes(item.read_bytes()) + else: + assert item.is_dir() + item_out.mkdir() + stack.push((item, item_out)) + + return root + +static_folder = extract_importlib(protoserve_data) +app = Quart(__name__, static_folder=static_folder.name) + +@app.route('/') +async def index(): + return await app.send_static_file('protoserve.html') + +def deserialize(obj, unit): + pitch_x = float(obj.get('pitch_x', 1.27)) + pitch_y = float(obj.get('pitch_y', 1.27)) + clearance = float(obj.get('clearance', 0.2)) + + match obj['type']: + case 'layout': + proportions = [float(child['layout_prop']) for child in obj['children']] + content = [deserialize(child, unit) for child in obj['children']] + return pb.PropLayout(content, obj['direction'], proportions) + + case 'placeholder': + return pb.EmptyProtoArea() + + case 'smd': + match obj['pad_shape']: + case 'rect': + pad = pb.SMDPad.rect(0, 0, pitch_x-clearance, pitch_y-clearance, paste=False, unit=unit) + case 'circle': + pad = pb.SMDPad.circle(0, 0, min(pitch_x, pitch_y)-clearance, paste=False, unit=unit) + return pb.PatternProtoArea(pitch_x, pitch_y, obj=pad, unit=unit) + + case 'tht': + hole_dia = float(obj['hole_dia']) + match obj['plating']: + case 'plated': + oneside, plated = False, True + case 'nonplated': + oneside, plated = False, False + case 'singleside': + oneside, plated = True, False + + match obj['pad_shape']: + case 'rect': + pad = pb.THTPad.rect(0, 0, hole_dia, pitch_x-clearance, pitch_y-clearance, paste=False, plated=plated, unit=unit) + case 'circle': + pad = pb.THTPad.circle(0, 0, hole_dia, min(pitch_x, pitch_y)-clearance, paste=False, plated=plated, unit=unit) + case 'obround': + pad = pb.THTPad.obround(0, 0, hole_dia, pitch_x-clearance, pitch_y-clearance, paste=False, plated=plated, unit=unit) + + if oneside: + pad.pad_bottom = None + + return pb.PatternProtoArea(pitch_x, pitch_y, obj=pad, unit=unit) + + case 'manhattan': + return pb.PatternProtoArea(pitch_x, pitch_y, obj=pb.ManhattanPads(pitch_x, pitch_y, clearance, unit=unit), unit=unit) + + case 'powered': + pitch = float(obj.get('pitch', 2.54)) + hole_dia = float(obj['hole_dia']) + via_drill = float(obj['via_hole_dia']) + trace_width = float(obj['trace_width']) + return pb.PatternProtoArea(pitch, pitch, pb.PoweredProto(pitch, hole_dia, clearance, via_size=via_drill, trace_width=trace_width, unit=unit), unit=unit) + + case 'flower': + pitch = float(obj.get('pitch', 2.54)) + hole_dia = float(obj['hole_dia']) + pattern_dia = float(obj['pattern_dia']) + return pb.PatternProtoArea(2*pitch, 2*pitch, pb.THTFlowerProto(pitch, hole_dia, pattern_dia, unit=unit), unit=unit) + + case 'rf': + pitch = float(obj.get('pitch', 2.54)) + hole_dia = float(obj['hole_dia']) + via_dia = float(obj['via_dia']) + via_drill = float(obj['via_hole_dia']) + return pb.PatternProtoArea(pitch, pitch, pb.RFGroundProto(pitch, hole_dia, clearance, via_dia, via_drill, unit=MM), unit=MM) + +@app.route('/preview.svg', methods=['POST']) +async def preview(): + obj = await request.get_json() + + unit = Inch if obj.get('units' == 'us') else MM + w = float(obj.get('width', unit(100, MM))) + h = float(obj.get('height', unit(80, MM))) + corner_radius = float(obj.get('round_corners', {}).get('radius', unit(1.5, MM))) + holes = obj.get('mounting_holes', {}) + mounting_hole_dia = float(holes.get('diameter', unit(3.2, MM))) + mounting_hole_offset = float(holes.get('offset', unit(5, MM))) + + content = deserialize(obj['children'][0], unit) + + board = pb.ProtoBoard(w, h, content, + corner_radius=corner_radius, + mounting_hole_dia=mounting_hole_dia, + mounting_hole_offset=mounting_hole_offset, + unit=unit) + return Response(str(board.pretty_svg()), mimetype='image/svg+xml') + + +if __name__ == '__main__': + app.run() + diff --git a/gerbonara/cad/protoserve_data/protoserve.html b/gerbonara/cad/protoserve_data/protoserve.html new file mode 100644 index 0000000..4b116b2 --- /dev/null +++ b/gerbonara/cad/protoserve_data/protoserve.html @@ -0,0 +1,899 @@ +<!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%; +} + +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 > 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; +} + +.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 { + grid-area: main; +} + +#preview-image { + width: 100%; + height: 100%; + object-fit: contain; +} + +#links { + grid-area: links; +} + +.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%); +} + +</style> + </head> + <body> + <div id="controls"> + <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"> + <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"> + <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"> + <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"></input> + <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"></input> + <span class="unit metric">mm</span> + <span class="unit us">inch</span> + </label> + </div> + + <h4>Content</h4> + <div class="group placeholder"></div> + </div> + </div> + <div id="preview"> + <img id="preview-image" alt="Automatically generated preview image"/> + </div> + <div id="links"> + <a href="#controls">Settings</a> + <a href="#preview">Preview</a> + <a href='/download'> + <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"> + </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 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"> + </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"> + </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> + </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"> + </label> + + <h5>Area Settings</h5> + <label>Pitch X + <input type="text" name="pitch_x" placeholder="length" value="1.27"> + <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"> + <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"> + <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> + <option value="obround">Obround</option> + </select> + </label> + <label class="only-shape rect">Corner radius + <input type="text" name="pad_h" placeholder="length" value="0"> + <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"> + </label> + + <h5>Area Settings</h5> + <label>Pitch X + <input type="text" name="pitch_x" placeholder="length" value="2.54"> + <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"> + <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"> + <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"> + <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"> + <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"> + </label> + + <h5>Area Settings</h5> + <label>Pitch X + <input type="text" name="pitch_x" placeholder="length" value="5.08"> + <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"> + <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"> + <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"> + </label> + + <h5>Area Settings</h5> + <label>Pitch + <input type="text" name="pitch" placeholder="length" value="2.54"> + <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"> + <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"> + <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"> + <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"> + </label> + + <h5>Area Settings</h5> + <label>Pitch + <input type="text" name="pitch" placeholder="length" value="2.54"> + <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"> + <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.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"> + <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"> + <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"> + </label> + + <h5>Area Settings</h5> + <label>Pitch + <input type="text" name="pitch" placeholder="length" value="2.54"> + <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"> + <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"> + <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"> + <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"> + <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"> + <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); + } + } + } + } + + previewBlobURL = null; + previewReloader = new RateLimiter(async () => { + 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; + }, 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); + } + }); + } + + hookupPreviewUpdate(document.querySelector('.group.board')); + previewReloader.scheduleCall(); + </script> + </body> +</html> |