Mixed progress

- point tagging
- extends clauses for drier config
- classic/uniform support
- mx/choc spacing support
- intersect outlines
- per-key footprint customization
This commit is contained in:
Bán Dénes 2020-07-16 23:55:37 +02:00
parent 55d60ba599
commit c3d7643371
6 changed files with 305 additions and 141 deletions

View file

@ -324,6 +324,7 @@ This is where the following section comes into play:
```yaml ```yaml
glue: glue:
glue_name:
top: top:
left: <anchor> left: <anchor>
right: <anchor> | num right: <anchor> | num
@ -337,6 +338,7 @@ glue:
extra: extra:
- <primitive shape> - <primitive shape>
- ... - ...
...
``` ```
...where an `<anchor>` is (mostly) the same as it was for points: ...where an `<anchor>` is (mostly) the same as it was for points:
@ -348,7 +350,7 @@ rotate: num # default = 0
relative: boolean # default = true relative: boolean # default = true
``` ```
The section's `top` and `bottom` are both formatted the same, and describe the center line's top and bottom intersections, respectively. The `top` and `bottom` fields in each glue's section are both formatted the same, and describe the center line's top and bottom intersections, respectively.
In a one-piece case, this means that we project a line from a left-side reference point (optionally rotated and translated), another from the right, and converge them to where they meet. In a one-piece case, this means that we project a line from a left-side reference point (optionally rotated and translated), another from the right, and converge them to where they meet.
Split designs can specify `right` as a single number to mean the x coordinate where the side should be "cut off". Split designs can specify `right` as a single number to mean the x coordinate where the side should be "cut off".
The `relative` flag means that the `shift` is interpreted in layout size units instead of mms (see below). The `relative` flag means that the `shift` is interpreted in layout size units instead of mms (see below).
@ -366,7 +368,7 @@ If this is insufficient (maybe because it would leave holes), the `waypoints` ca
Here, `percent` means the y coordinate along the centerline (going from the top intersection to the bottom intersection), and `width` means the offset on the x axis. Here, `percent` means the y coordinate along the centerline (going from the top intersection to the bottom intersection), and `width` means the offset on the x axis.
If this is somehow _still_ insufficient (or there were problems with the binding phase), we can specify additional primitive shapes under the `extra` key (similarly to how we would use them in the exports; see below). If this is somehow _still_ insufficient (or there were problems with the binding phase), we can specify additional primitive shapes under the `extra` key (similarly to how we would use them in the exports; see below).
These are then added to what we have so far to finish out the glue. These are then added to what we have so far to finish out the glue. TODO!
<hr /> <hr />
@ -381,6 +383,8 @@ Now we can configure what we want to "export" as outlines from this phase, given
- `middle` means an "ideal" version of the glue (meaning that instead of the `outline.glue` we defined above, we get `both` - `left` - `right`, so the _exact_ middle piece we would have needed to glue everything together - `middle` means an "ideal" version of the glue (meaning that instead of the `outline.glue` we defined above, we get `both` - `left` - `right`, so the _exact_ middle piece we would have needed to glue everything together
- `both` means both sides, held together by the glue - `both` means both sides, held together by the glue
- `glue` is just the raw glue shape we defined above under `outline.glue` - `glue` is just the raw glue shape we defined above under `outline.glue`
- `tag: <array of tags>` : optional tags to filter which points to consider in this step, where tags can be specified as key-level attributes.
- `glue: <glue_name>` : the name of the glue to use, if applicable
- `size: num | [num_x, num_y]` : the width/height of the rectangles to lay onto the points. Note that the `relative` flag for the glue declaration above meant this size as the basis of the shift. So during a `keys` layout with a size of 18, for example, a relative shift of `[.5, .5]` actually means `[9, 9]` in mms. - `size: num | [num_x, num_y]` : the width/height of the rectangles to lay onto the points. Note that the `relative` flag for the glue declaration above meant this size as the basis of the shift. So during a `keys` layout with a size of 18, for example, a relative shift of `[.5, .5]` actually means `[9, 9]` in mms.
- `corner: num # default = 0)` : corner radius of the rectangles - `corner: num # default = 0)` : corner radius of the rectangles
- `bevel: num # default = 0)` : corner bevel of the rectangles, can be combined with rounding - `bevel: num # default = 0)` : corner bevel of the rectangles, can be combined with rounding
@ -395,7 +399,7 @@ Now we can configure what we want to "export" as outlines from this phase, given
- `radius: num` : the radius of the circle - `radius: num` : the radius of the circle
- `polygon` : an independent polygon primitive. Parameters: - `polygon` : an independent polygon primitive. Parameters:
- `points: [<point_def>, ...]` : the points of the polygon. Each `<point_def>` can have its own `ref` and `shift`, all of which are still the same as above. If `ref` is unspecified, the previous point's will be assumed. For the first, it's `[0, 0]` by default. - `points: [<point_def>, ...]` : the points of the polygon. Each `<point_def>` can have its own `ref` and `shift`, all of which are still the same as above. If `ref` is unspecified, the previous point's will be assumed. For the first, it's `[0, 0]` by default.
- `ref` : a previously defined outline, see below. - `outline` : a previously defined outline, see below.
- `name: outline_name` : the name of the referenced outline - `name: outline_name` : the name of the referenced outline
Using these, we define exports as follows: Using these, we define exports as follows:

View file

@ -1,4 +1,5 @@
const m = require('makerjs') const m = require('makerjs')
const u = require('./utils')
const Point = require('./point') const Point = require('./point')
const assert = exports.assert = (exp, msg) => { const assert = exports.assert = (exp, msg) => {
@ -79,11 +80,53 @@ exports.anchor = (raw, name, points={}, check_unexpected=true, default_point=new
point.shift(xyval, true) point.shift(xyval, true)
} }
if (raw.rotate !== undefined) { if (raw.rotate !== undefined) {
let rot = sane(raw.rotate || 0, name + '.rotate', 'number') point.r += sane(raw.rotate || 0, name + '.rotate', 'number')
if (point.meta.mirrored) {
rot = -rot
}
point.r += rot
} }
return point return point
} }
const extend_pair = exports.extend_pair = (to, from) => {
const to_type = type(to)
const from_type = type(from)
if (from === undefined || from === null) return to
if (from === '!!unset') return undefined
if (to_type != from_type) return from
if (from_type == 'object') {
const res = u.deepcopy(to)
for (const key of Object.keys(from)) {
res[key] = extend_pair(to[key], from[key])
}
return res
} else if (from_type == 'array') {
const res = u.deepcopy(to)
for (const [i, val] of from.entries()) {
res[i] = extend_pair(res[i], val)
}
return res
} else return from
}
exports.extend = (...args) => {
let res = args[0]
for (const arg of args) {
if (res == arg) continue
res = extend_pair(res, arg)
}
return res
}
const inherit = exports.inherit = (config, name_prefix, name, set) => {
let result = u.deepcopy(config)
if (config.extends !== undefined) {
let list = config.extends
if (type(list) !== 'array') list = [list]
for (const item of list) {
const other = set[item]
assert(other, `Field "${name_prefix}.${name}" does not name a valid target!`)
result = extend_pair(inherit(other, name_prefix, config.extends, set), result)
}
delete result.extends
}
return result
}

View file

@ -35,36 +35,45 @@ const layout = exports._layout = (config = {}, points = {}) => {
// Glue config sanitization // Glue config sanitization
a.detect_unexpected(config, 'outline.glue', ['top', 'bottom', 'waypoints', 'extra']) const parsed_glue = u.deepcopy(a.sane(config, 'outline.glue', 'object'))
for (let [gkey, gval] of Object.entries(parsed_glue)) {
gval = a.inherit(gval, 'outline.glue', gkey, config)
a.detect_unexpected(gval, `outline.glue.${gkey}`, ['top', 'bottom', 'waypoints', 'extra'])
for (const y of ['top', 'bottom']) { for (const y of ['top', 'bottom']) {
a.detect_unexpected(config[y], `outline.glue.${y}`, ['left', 'right']) a.detect_unexpected(gval[y], `outline.glue.${gkey}.${y}`, ['left', 'right'])
config[y].left = relative_anchor(config[y].left, `outline.glue.${y}.left`, points) gval[y].left = relative_anchor(gval[y].left, `outline.glue.${gkey}.${y}.left`, points)
if (a.type(config[y].right) != 'number') { if (a.type(gval[y].right) != 'number') {
config[y].right = relative_anchor(config[y].right, `outline.glue.${y}.right`, points) gval[y].right = relative_anchor(gval[y].right, `outline.glue.${gkey}.${y}.right`, points)
} }
} }
config.waypoints = a.sane(config.waypoints || [], 'outline.glue.waypoints', 'array') gval.waypoints = a.sane(gval.waypoints || [], `outline.glue.${gkey}.waypoints`, 'array')
let wi = 0 let wi = 0
config.waypoints = config.waypoints.map(w => { gval.waypoints = gval.waypoints.map(w => {
const name = `outline.glue.waypoints[${++wi}]` const name = `outline.glue.${gkey}.waypoints[${++wi}]`
a.detect_unexpected(w, name, ['percent', 'width']) a.detect_unexpected(w, name, ['percent', 'width'])
w.percent = a.sane(w.percent, name + '.percent', 'number') w.percent = a.sane(w.percent, name + '.percent', 'number')
w.width = a.wh(w.width, name + '.width') w.width = a.wh(w.width, name + '.width')
return w return w
}) })
parsed_glue[gkey] = gval
}
// TODO: handle glue.extra (or revoke it from the docs) // TODO: handle glue.extra (or revoke it from the docs)
return (params, export_name, expected) => { return (params, export_name, expected) => {
// Layout params sanitization // Layout params sanitization
a.detect_unexpected(params, `${export_name}`, expected.concat(['side', 'size', 'corner', 'bevel', 'bound'])) a.detect_unexpected(params, `${export_name}`, expected.concat(['side', 'tags', 'glue', 'size', 'corner', 'bevel', 'bound']))
const side = a.in(params.side, `${export_name}.side`, ['left', 'right', 'middle', 'both', 'glue']) const side = a.in(params.side, `${export_name}.side`, ['left', 'right', 'middle', 'both', 'glue'])
const tags = a.sane(params.tags || [], `${export_name}.tags`, 'array')
const default_glue_name = Object.keys(parsed_glue)[0]
const glue_def = parsed_glue[a.sane(params.glue || default_glue_name, `${export_name}.glue`, 'string')]
a.assert(glue_def, `Field "${export_name}.glue" does not name a valid glue!`)
const size = a.wh(params.size, `${export_name}.size`) const size = a.wh(params.size, `${export_name}.size`)
const corner = a.sane(params.corner || 0, `${export_name}.corner`, 'number') const corner = a.sane(params.corner || 0, `${export_name}.corner`, 'number')
const bevel = a.sane(params.bevel || 0, `${export_name}.bevel`, 'number') const bevel = a.sane(params.bevel || 0, `${export_name}.bevel`, 'number')
@ -77,6 +86,14 @@ const layout = exports._layout = (config = {}, points = {}) => {
if (['left', 'right', 'middle', 'both'].includes(side)) { if (['left', 'right', 'middle', 'both'].includes(side)) {
for (const [pname, p] of Object.entries(points)) { for (const [pname, p] of Object.entries(points)) {
// filter by tags, if necessary
if (tags.length) {
const source = p.meta.tags || {}
const point_tags = Object.keys(source).filter(t => !!source[t])
const relevant = point_tags.some(pt => tags.includes(pt))
if (!relevant) continue
}
let from_x = -size[0] / 2, to_x = size[0] / 2 let from_x = -size[0] / 2, to_x = size[0] / 2
let from_y = -size[1] / 2, to_y = size[1] / 2 let from_y = -size[1] / 2, to_y = size[1] / 2
@ -131,14 +148,14 @@ const layout = exports._layout = (config = {}, points = {}) => {
return u.line(from.p, to.p) return u.line(from.p, to.p)
} }
const tll = get_line(config.top.left) const tll = get_line(glue_def.top.left)
const trl = get_line(config.top.right) const trl = get_line(glue_def.top.right)
const tip = m.path.converge(tll, trl) const tip = m.path.converge(tll, trl)
const tlp = u.eq(tll.origin, tip) ? tll.end : tll.origin const tlp = u.eq(tll.origin, tip) ? tll.end : tll.origin
const trp = u.eq(trl.origin, tip) ? trl.end : trl.origin const trp = u.eq(trl.origin, tip) ? trl.end : trl.origin
const bll = get_line(config.bottom.left) const bll = get_line(glue_def.bottom.left)
const brl = get_line(config.bottom.right) const brl = get_line(glue_def.bottom.right)
const bip = m.path.converge(bll, brl) const bip = m.path.converge(bll, brl)
const blp = u.eq(bll.origin, bip) ? bll.end : bll.origin const blp = u.eq(bll.origin, bip) ? bll.end : bll.origin
const brp = u.eq(brl.origin, bip) ? brl.end : brl.origin const brp = u.eq(brl.origin, bip) ? brl.end : brl.origin
@ -146,7 +163,7 @@ const layout = exports._layout = (config = {}, points = {}) => {
const left_waypoints = [] const left_waypoints = []
const right_waypoints = [] const right_waypoints = []
for (const w of config.waypoints) { for (const w of glue_def.waypoints) {
const percent = w.percent / 100 const percent = w.percent / 100
const center_x = tip[0] + percent * (bip[0] - tip[0]) const center_x = tip[0] + percent * (bip[0] - tip[0])
const center_y = tip[1] + percent * (bip[1] - tip[1]) const center_y = tip[1] + percent * (bip[1] - tip[1])
@ -157,7 +174,7 @@ const layout = exports._layout = (config = {}, points = {}) => {
} }
let waypoints let waypoints
const is_split = a.type(config.top.right) == 'number' const is_split = a.type(glue_def.top.right) == 'number'
if (is_split) { if (is_split) {
waypoints = [tip, tlp] waypoints = [tip, tlp]
.concat(left_waypoints) .concat(left_waypoints)
@ -198,7 +215,7 @@ exports.parse = (config = {}, points = {}) => {
for (const part of parts) { for (const part of parts) {
const name = `outline.exports.${key}[${++index}]` const name = `outline.exports.${key}[${++index}]`
const expected = ['type', 'operation'] const expected = ['type', 'operation']
part.type = a.in(part.type, `${name}.type`, ['keys', 'rectangle', 'circle', 'polygon', 'ref']) part.type = a.in(part.type, `${name}.type`, ['keys', 'rectangle', 'circle', 'polygon', 'outline'])
part.operation = a.in(part.operation || 'add', `${name}.operation`, ['add', 'subtract', 'intersect', 'stack']) part.operation = a.in(part.operation || 'add', `${name}.operation`, ['add', 'subtract', 'intersect', 'stack'])
let op = u.union let op = u.union
@ -240,10 +257,12 @@ exports.parse = (config = {}, points = {}) => {
} }
arg = u.poly(parsed_points) arg = u.poly(parsed_points)
break break
case 'ref': case 'outline':
a.assert(outlines[part.name], `Field "${name}.name" does not name an existing outline!`) a.assert(outlines[part.name], `Field "${name}.name" does not name an existing outline!`)
arg = u.deepcopy(outlines[part.name]) arg = u.deepcopy(outlines[part.name])
break break
default:
throw new Error(`Field "${name}.type" (${part.type}) does not name a valid outline part type!`)
} }
result = op(result, arg) result = op(result, arg)

View file

@ -144,6 +144,8 @@ const makerjs2kicad = exports._makerjs2kicad = (model, layer='Edge.Cuts') => {
const footprint_types = require('./footprints') const footprint_types = require('./footprints')
const footprint = exports._footprint = (config, name, points, net_indexer, point) => { const footprint = exports._footprint = (config, name, points, net_indexer, point) => {
if (config === false) return ''
// config sanitization // config sanitization
a.detect_unexpected(config, name, ['type', 'anchor', 'nets', 'params']) a.detect_unexpected(config, name, ['type', 'anchor', 'nets', 'params'])
const type = a.in(config.type, `${name}.type`, Object.keys(footprint_types)) const type = a.in(config.type, `${name}.type`, Object.keys(footprint_types))

View file

@ -2,35 +2,6 @@ const m = require('makerjs')
const u = require('./utils') const u = require('./utils')
const a = require('./assert') const a = require('./assert')
const extend_pair = exports._extend_pair = (to, from) => {
const to_type = a.type(to)
const from_type = a.type(from)
if (from === undefined || from === null) return to
if (to_type != from_type) return from
if (from_type == 'object') {
const res = u.deepcopy(to)
for (const key of Object.keys(from)) {
res[key] = extend_pair(to[key], from[key])
}
return res
} else if (from_type == 'array') {
const res = u.deepcopy(to)
for (const [i, val] of from.entries()) {
res[i] = extend_pair(res[i], val)
}
return res
} else return from
}
const extend = exports._extend = (...args) => {
let res = args[0]
for (const arg of args) {
if (res == arg) continue
res = extend_pair(res, arg)
}
return res
}
const push_rotation = exports._push_rotation = (list, angle, origin) => { const push_rotation = exports._push_rotation = (list, angle, origin) => {
let candidate = origin let candidate = origin
for (const r of list) { for (const r of list) {
@ -139,7 +110,7 @@ const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key)
asym: 'both' asym: 'both'
} }
for (const row of Object.keys(actual_rows)) { for (const row of Object.keys(actual_rows)) {
const key = extend( const key = a.extend(
default_key, default_key,
global_key, global_key,
zone_wide_key, zone_wide_key,
@ -148,7 +119,8 @@ const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key)
col.rows[row] || {} col.rows[row] || {}
) )
key.name = key.name || `${col_name}_${row}` key.name = key.name || `${zone_name}_${col_name}_${row}`
key.colrow = `${col_name}_${row}`
key.shift = a.xy(key.shift, `${key.name}.shift`) key.shift = a.xy(key.shift, `${key.name}.shift`)
key.rotate = a.sane(key.rotate, `${key.name}.rotate`, 'number') key.rotate = a.sane(key.rotate, `${key.name}.rotate`, 'number')
key.padding = a.sane(key.padding, `${key.name}.padding`, 'number') key.padding = a.sane(key.padding, `${key.name}.padding`, 'number')
@ -195,8 +167,6 @@ const render_zone = exports._render_zone = (zone_name, zone, anchor, global_key)
return points return points
} }
exports.parse = (config = {}) => { exports.parse = (config = {}) => {
a.detect_unexpected(config, 'points', ['zones', 'key', 'rotate', 'mirror']) a.detect_unexpected(config, 'points', ['zones', 'key', 'rotate', 'mirror'])
@ -207,7 +177,11 @@ exports.parse = (config = {}) => {
const zones = a.sane(config.zones || {}, 'points.zones', 'object') const zones = a.sane(config.zones || {}, 'points.zones', 'object')
const global_key = a.sane(config.key || {}, 'points.key', 'object') const global_key = a.sane(config.key || {}, 'points.key', 'object')
for (const [zone_name, zone] of Object.entries(zones)) { for (let [zone_name, zone] of Object.entries(zones)) {
// handle zone-level `extends` clauses
zone = a.inherit(zone, 'points.zones', zone_name, zones)
const anchor = a.anchor(zone.anchor || {}, `points.zones.${zone_name}.anchor`, points) const anchor = a.anchor(zone.anchor || {}, `points.zones.${zone_name}.anchor`, points)
points = Object.assign(points, render_zone(zone_name, zone, anchor, global_key)) points = Object.assign(points, render_zone(zone_name, zone, anchor, global_key))
} }
@ -238,9 +212,11 @@ exports.parse = (config = {}) => {
for (const [name, p] of Object.entries(points)) { for (const [name, p] of Object.entries(points)) {
if (p.meta.asym == 'left') continue if (p.meta.asym == 'left') continue
const mp = p.clone().mirror(axis) const mp = p.clone().mirror(axis)
mp.meta = extend(mp.meta, mp.meta.mirror || {}) mp.meta = a.extend(mp.meta, mp.meta.mirror || {})
mp.meta.mirrored = true mp.meta.mirrored = true
mirrored_points[`mirror_${name}`] = mp const new_name = `mirror_${name}`
mp.meta.name = new_name
mirrored_points[new_name] = mp
if (p.meta.asym == 'right') { if (p.meta.asym == 'right') {
p.meta.skip = true p.meta.skip = true
} }

View file

@ -10,9 +10,9 @@ points:
rows: rows:
bottom: bottom:
home: home:
bind: [,10] bind: [,15]
top: top:
bind: [,10] bind: [,15]
key: key:
column_net: P1 column_net: P1
ring: ring:
@ -61,21 +61,35 @@ points:
rows: rows:
bottom: bottom:
bind: [10] bind: [10]
row_net: P7
mirror:
row_net: P16 row_net: P16
mirror:
row_net: P7
home: home:
bind: [10] bind: [10]
row_net: P6
mirror:
row_net: P14 row_net: P14
top:
row_net: P5
mirror: mirror:
row_net: P6
top:
row_net: P15 row_net: P15
mirror:
row_net: P5
key:
tags:
s19: true
choc:
extends: matrix
columns:
pinky:
stagger: 1
origin: [7, -8]
key:
padding: 18
tags:
s19: false
s18: true
thumbfan: thumbfan:
anchor: anchor:
ref: inner_bottom ref: matrix_inner_bottom
shift: [-7, -19] shift: [-7, -19]
columns: columns:
near: near:
@ -84,9 +98,11 @@ points:
origin: [9.5, -9] origin: [9.5, -9]
rows: rows:
thumb: thumb:
bind: [10,1,,] bind: [10,5,,]
key: key:
column_net: P2 column_net: P2
tags:
classic: true
home: home:
spread: 21.25 spread: 21.25
rotate: -28 rotate: -28
@ -96,17 +112,71 @@ points:
bind: [,10,,15] bind: [,10,,15]
key: key:
column_net: P3 column_net: P3
tags:
classic: true
uniform: true
far: far:
rows: rows:
thumb: thumb:
bind: [-1,,,5] bind: [-1,,,5]
key: key:
column_net: P4 column_net: P4
tags:
classic: true
rows: rows:
thumb: thumb:
row_net: P8
mirror:
row_net: P10 row_net: P10
mirror:
row_net: P8
unifar:
anchor:
ref: thumbfan_home_thumb
columns:
home_again:
rotate: -28
origin: [9.5, -9]
key:
skip: true
far1u:
rows:
thumb:
bind: [-1,,,5]
key:
column_net: P4
footprints:
diode: false
tags:
uniform: true
rows:
thumb:
row_net: P10
mirror:
row_net: P8
uninear:
anchor:
ref: thumbfan_home_thumb
columns:
home_again:
spread: -19
rotate: 28
origin: [-9.5, -9]
key:
skip: true
near1u:
rows:
thumb:
bind: [10,5,,]
key:
column_net: P2
footprints:
diode: false
tags:
uniform: true
rows:
thumb:
row_net: P10
mirror:
row_net: P8
key: key:
bind: [0,0,0,0] bind: [0,0,0,0]
footprints: footprints:
@ -114,35 +184,42 @@ points:
type: mx type: mx
nets: nets:
from: '!column_net' from: '!column_net'
to: '!name' to: '!colrow'
diode: diode:
type: diode type: diode
anchor: anchor:
rotate: 90 rotate: 90
shift: [-8, 0] shift: [8, 0]
nets: nets:
from: '!name' from: '!name'
to: '!row_net' to: '!row_net'
mirror:
footprints:
mx:
nets:
from: '!colrow'
to: '!column_net'
rotate: -20 rotate: -20
mirror: mirror:
ref: pinky_home ref: matrix_pinky_home
distance: 223.7529778 distance: 223.7529778
outline: outline:
glue: glue:
classic:
top: top:
left: left:
ref: inner_top ref: matrix_inner_top
shift: [, 0.5] shift: [, 0.5]
right: right:
ref: mirror_inner_top ref: mirror_matrix_inner_top
shift: [, 0.5] shift: [, 0.5]
bottom: bottom:
left: left:
ref: far_thumb ref: thumbfan_far_thumb
shift: [0.5, 0] shift: [0.5, 0]
rotate: 90 rotate: 90
right: right:
ref: mirror_far_thumb ref: mirror_thumbfan_far_thumb
shift: [0.5, 0] shift: [0.5, 0]
rotate: 90 rotate: 90
waypoints: waypoints:
@ -150,59 +227,102 @@ outline:
width: 50 width: 50
- percent: 90 - percent: 90
width: 25 width: 25
uniform:
extends: classic
bottom:
left:
ref: unifar_far1u_thumb
right:
ref: mirror_unifar_far1u_thumb
choc:
extends: classic
top:
left:
ref: choc_inner_top
right:
ref: mirror_choc_inner_top
uniform_choc:
extends:
- uniform
- choc
exports: exports:
outline: classic_outline:
- type: keys - type: keys
side: both side: both
size: 18 tags:
- s19
- classic
glue: classic
size: 13.5
corner: .5 corner: .5
holes: uniform_outline:
- type: keys - type: keys
operation: stack
side: both side: both
tags:
- s19
- uniform
glue: uniform
size: 13.5
corner: .5
intersected_outline:
- type: outline
name: classic_outline
- type: outline
name: uniform_outline
operation: intersect
classic_holes:
- type: keys
side: both
tags:
- s19
- classic
glue: classic
size: 14 size: 14
bound: false bound: false
middle: classic_middle:
- type: keys - type: keys
operation: stack
side: middle side: middle
tags:
- s19
- classic
glue: classic
size: 24 size: 24
- type: rectangle - type: rectangle
size: [25, 5] size: [25, 5]
ref: home_thumb ref: thumbfan_home_thumb
shift: [0, 12] shift: [0, 12]
- type: rectangle - type: rectangle
size: [25, 5] size: [25, 5]
ref: far_thumb ref: thumbfan_far_thumb
shift: [25, 12] shift: [25, 12]
- type: rectangle - type: rectangle
size: [25, 5] size: [25, 5]
ref: mirror_home_thumb ref: mirror_thumbfan_home_thumb
shift: [25, 12] shift: [25, 12]
- type: rectangle - type: rectangle
size: [25, 5] size: [25, 5]
ref: mirror_far_thumb ref: mirror_thumbfan_far_thumb
shift: [0, 12] shift: [0, 12]
- type: ref - type: outline
name: outline name: classic_outline
operation: intersect operation: intersect
complex: complex:
- type: ref - type: outline
name: outline name: classic_outline
- type: ref - type: outline
name: holes name: classic_holes
operation: stack operation: stack
- type: ref - type: outline
name: middle name: classic_middle
operation: stack operation: stack
pcb: pcb:
edge: outline edge: intersected_outline
footprints: footprints:
mcu: mcu:
type: promicro type: promicro
anchor: anchor:
ref: ref:
- inner_top - choc_inner_top
- mirror_inner_top - mirror_choc_inner_top
shift: [0, -20] shift: [0, -20]
rotate: 270 rotate: 270