protogen web interface works
This commit is contained in:
parent
e18dbb11f8
commit
fba189c695
5 changed files with 1055 additions and 588 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
131
gerbonara/cad/protoserve.py
Normal file
131
gerbonara/cad/protoserve.py
Normal file
|
|
@ -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()
|
||||
|
||||
899
gerbonara/cad/protoserve_data/protoserve.html
Normal file
899
gerbonara/cad/protoserve_data/protoserve.html
Normal file
|
|
@ -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>
|
||||
|
|
@ -1,568 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Protoserve</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{url_for('static', filename='pure-min.css')}}">
|
||||
<link rel="icon" type="image/png" href="{{url_for('static', filename='favicon-512.png')}}">
|
||||
<link rel="apple-touch-icon" href="{{url_for('static', filename='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;
|
||||
}
|
||||
|
||||
#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 id="g-board-units">Units
|
||||
<select value="metric">
|
||||
<option value="metric">Metric</option>
|
||||
<option value="us">US Customary</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label id="g-board-w">Board width
|
||||
<input id="input-board-w" type="text" placeholder="width" value="100">
|
||||
<span class="unit metric">mm</span>
|
||||
<span class="unit us">inch</span>
|
||||
</label>
|
||||
|
||||
<label id="g-board-h">Board height
|
||||
<input id="input-board-h" type="text" placeholder="height" value="80">
|
||||
<span class="unit metric">mm</span>
|
||||
<span class="unit us">inch</span>
|
||||
</label>
|
||||
|
||||
<div class="group expand">
|
||||
<label id="g-round-corners">Round corners
|
||||
<input type="checkbox" checked>
|
||||
</label>
|
||||
|
||||
<label id="g-board-corner-radius">Radius
|
||||
<input 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">
|
||||
<label>Mounting holes
|
||||
<input type="checkbox" name="has_holes" checked>
|
||||
</label>
|
||||
|
||||
<label>Diameter
|
||||
<input type="text" placeholder="diameter" name="hole_dia" value="3.2"></input>
|
||||
<span class="unit metric">mm</span>
|
||||
<span class="unit us">inch</span>
|
||||
</label>
|
||||
|
||||
<label>Board edge to hole center (X)
|
||||
<input type="text" placeholder="distance" name="hole_off_x" value="5"></input>
|
||||
<span class="unit metric">mm</span>
|
||||
<span class="unit us">inch</span>
|
||||
</label>
|
||||
|
||||
<label>Board edge to hole center (Y)
|
||||
<input type="text" placeholder="distance" name="hole_off_y" 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 src="/preview.svg" alt="Automaticallly 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 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 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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-g-smd">
|
||||
<div 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">
|
||||
<span class="unit metric">mm</span>
|
||||
<span class="unit us">mil</span>
|
||||
</label>
|
||||
<label>Pitch Y
|
||||
<input type="text" name="pitch_y" placeholder="length">
|
||||
<span class="unit metric">mm</span>
|
||||
<span class="unit us">mil</span>
|
||||
</label>
|
||||
<label>Clearance
|
||||
<input type="text" name="clearance" placeholder="length">
|
||||
<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">Rectangle</option>
|
||||
<option value="circle">Circle</option>
|
||||
<option value="obround">Obround</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Pad width
|
||||
<input type="text" name="pad_w" placeholder="length">
|
||||
<span class="unit metric">mm</span>
|
||||
<span class="unit us">mil</span>
|
||||
</label>
|
||||
<label>Pad height
|
||||
<input type="text" name="pad_h" placeholder="length">
|
||||
<span class="unit metric">mm</span>
|
||||
<span class="unit us">mil</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-g-tht">
|
||||
<div 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">
|
||||
<span class="unit metric">mm</span>
|
||||
<span class="unit us">mil</span>
|
||||
</label>
|
||||
<label>Pitch Y
|
||||
<input type="text" name="pitch_y" placeholder="length">
|
||||
<span class="unit metric">mm</span>
|
||||
<span class="unit us">mil</span>
|
||||
</label>
|
||||
<label>Clearance
|
||||
<input type="text" name="clearance" placeholder="length">
|
||||
<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">Rectangle</option>
|
||||
<option value="circle">Circle</option>
|
||||
<option value="obround">Obround</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Pad width
|
||||
<input type="text" name="pad_w" placeholder="length">
|
||||
<span class="unit metric">mm</span>
|
||||
<span class="unit us">mil</span>
|
||||
</label>
|
||||
<label>Pad height
|
||||
<input type="text" name="pad_h" placeholder="length">
|
||||
<span class="unit metric">mm</span>
|
||||
<span class="unit us">mil</span>
|
||||
</label>
|
||||
<label class="shape-rect">Corner radius
|
||||
<input type="text" name="pad_h" placeholder="length">
|
||||
<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) => {
|
||||
console.log(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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
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 createPlaceholder() {
|
||||
const node = document.querySelector('#tpl-g-placeholder').content.cloneNode(true);
|
||||
|
||||
hookupAreaRemove(node);
|
||||
hookupAreaMove(node);
|
||||
|
||||
for (const bt of node.querySelectorAll('.placeholder a[data-placeholder]')) {
|
||||
bt.addEventListener('click', (evt) => {
|
||||
const template = document.querySelector(`#tpl-g-${evt.target.getAttribute('data-placeholder')}`);
|
||||
const node = template.content.cloneNode(true);
|
||||
|
||||
for (const elem of node.querySelectorAll('div.placeholder')) {
|
||||
elem.replaceWith(createPlaceholder());
|
||||
}
|
||||
|
||||
for (const elem of node.querySelectorAll('div.drop-target')) {
|
||||
elem.replaceWith(createDropTarget());
|
||||
}
|
||||
|
||||
hookupAreaRemove(node);
|
||||
hookupAreaMove(node);
|
||||
|
||||
for (const bt of node.querySelectorAll('a.add-element')) {
|
||||
bt.addEventListener('click', (evt) => {
|
||||
evt.target.before(createPlaceholder());
|
||||
evt.target.before(createDropTarget());
|
||||
});
|
||||
}
|
||||
|
||||
evt.target.closest('.group').replaceWith(node);
|
||||
});
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
document.querySelector('div.placeholder').replaceWith(createPlaceholder());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue