Bringing points stuff up to spec

This commit is contained in:
Bán Dénes 2020-06-26 21:00:10 +02:00
parent 0ab5a246e5
commit a5e686b059
11 changed files with 474 additions and 108 deletions

3
.gitignore vendored
View file

@ -114,3 +114,6 @@ dist
.yarn/build-state.yml .yarn/build-state.yml
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
# Project specific
output

View file

@ -87,7 +87,7 @@ In the following, we'll have an in-depth discussion about each of these, with an
## Points ## Points
A point in this context refers to a 2D point `[x,y]` with a rotation/orientation `r` added in. A point in this context refers to a 2D point `[x,y]` with a rotation/orientation `r` added in.
These can be thought of as the middle points of the keycaps in a resulting keyboard layout, with an additional handling of and angle of the keycap. These can be thought of as the middle points of the keycaps in a resulting keyboard layout, with an additional handling of the angle of the keycap.
What makes this generator "ergo" is the implicit focus on the column-stagger. What makes this generator "ergo" is the implicit focus on the column-stagger.
Of course we could simulate the traditional row-stagger by defining everything with a 90 degree rotation, but that's really not the goal here. Of course we could simulate the traditional row-stagger by defining everything with a 90 degree rotation, but that's really not the goal here.
@ -172,7 +172,7 @@ But if `key = {a: 1}` is extended by `key = {b: 2}`, the result is `key = {a: 1,
Lastly, while there are a few key-specific attributes that have special meaning in the context of points (listed below), any key with any data can be specified here. Lastly, while there are a few key-specific attributes that have special meaning in the context of points (listed below), any key with any data can be specified here.
This can be useful for storing arbitrary meta-info about the keys, or just configuring later stages with key-level parameters. This can be useful for storing arbitrary meta-info about the keys, or just configuring later stages with key-level parameters.
So, for example, when the outline phase specifies `bind` as a key-level parameter, it means that the global value can be extended just like any other key-level attribute. So, for example, when the outline phase specifies `bind` as a key-level parameter (see below), it means that the global value can be extended just like any other key-level attribute.
Now for the "official" key-level attributes: Now for the "official" key-level attributes:
@ -181,13 +181,15 @@ name: name_override # default = a concatenation of column and row
shift: [x, y] # default = [0, 0] shift: [x, y] # default = [0, 0]
rotate: num # default = 0 rotate: num # default = 0
origin: [x, y] # default = the center of the key origin: [x, y] # default = the center of the key
padding: num # default = 19
skip: boolean # default = false skip: boolean # default = false
asym: left|right|both # default = both asym: left | right | both # default = both
``` ```
`name` is the unique identifier of this specific key. `name` is the unique identifier of this specific key.
It defaults to a `<row>_<column>` format, but can be overridden if necessary. It defaults to a `<row>_<column>` format, but can be overridden if necessary.
`shift` and `rotate`/`origin` declare an extra, key-level translation or rotation, respectively. `shift` and `rotate`/`origin` declare an extra, key-level translation or rotation, respectively.
Then we leave `padding` amount of vertical space before moving on to the next key in the column.
`skip` signals that the point is just a "helper" and should not be included in the output. `skip` signals that the point is just a "helper" and should not be included in the output.
This can happen when a _real_ point is more easily calculable through a "stepping stone", but then we don't actually want the stepping stone to be a key itself. This can happen when a _real_ point is more easily calculable through a "stepping stone", but then we don't actually want the stepping stone to be a key itself.
Finally, `asym` relates to mirroring, which we'll cover in a second. Finally, `asym` relates to mirroring, which we'll cover in a second.
@ -339,13 +341,11 @@ ref: <point reference>
shift: [x, y] shift: [x, y]
rotate: num rotate: num
origin: [x, y] origin: [x, y]
relative: boolean # default = false
``` ```
The section's `top` and `bottom` are both formatted the same, and describe the center line's top and bottom intersections, respectively. The section's `top` and `bottom` 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 the unit of the translation specified in `shift` is not mm, but the size the point is laid out with; see below.)
This leads to a gluing middle patch that can be used to meld the left and right sides together, given by the counter-clockwise polygon: This leads to a gluing middle patch that can be used to meld the left and right sides together, given by the counter-clockwise polygon:
@ -371,13 +371,13 @@ Now we can configure what we want to "export" as outlines from this phase, given
- `all` : the combined outline that we've just created. Its parameters include: - `all` : the combined outline that we've just created. Its parameters include:
- `size: num | [num_x, num_y]` : the width/height of the rectangles to lay onto the points - `size: num | [num_x, num_y]` : the width/height of the rectangles to lay onto the points
- `corner: num # default = 0)` : corner radius of the rectangle - `corner: num # default = 0)` : corner radius of the rectangle
- `corner_style: rounded | beveled # default = rounded)` : the styleof the rectangle's corners - `bevel: num # default = 0)` : corner bevel of the rectangle, can be combined with rounding
- `keys` : only one side of the laid out keys, without the glue. Parameters: - `keys` : only one side of the laid out keys, without the glue. Parameters:
- everything we could specify for `all` - everything we could specify for `all`
- `side: left | right` : the side we want - `side: left | right` : the side we want
- `glue` : just the glue, but the "ideal" version of it. This means that instead of the `glue` we defined above, we get `all` - `left` - `right`, so the _exact_ middle piece we would have needed to glue everything together. Parameters: - `glue` : just the glue, but the "ideal" version of it. This means that instead of the `glue` we defined above, we get `all` - `left` - `right`, so the _exact_ middle piece we would have needed to glue everything together. Parameters:
- everything we could specify for `all` (since those are needed for the calculation) - everything we could specify for `all` (since those are needed for the calculation)
- `side: left | right | both # default = both)` : optionally, wecould choose only one side of the glue as well - `side: left | right | both # default = both)` : optionally, we could choose only one side of the glue as well
Additionally, we can use primitive shapes: Additionally, we can use primitive shapes:
@ -396,7 +396,6 @@ Additionally, we can use primitive shapes:
Using these, we define exports as follows: Using these, we define exports as follows:
```yaml ```yaml
exports: exports:
my_name: my_name:
@ -476,7 +475,7 @@ If we only want to use it as a building block for further exports, we can start
## Case ## Case
Cases add a pretty basic and minimal 3D aspect to the generation process. Cases add a pretty basic and minimal 3D aspect to the generation process.
In this phase, we take different outlines (exported from the above section, even the "private" ones), extrude and position them in space, optionally add some chamfer to the edges, and combine them into one 3D-printable object. In this phase, we take different outlines (exported from the above section, even the "private" ones), extrude and position them in space, and combine them into one 3D-printable object.
That's it. That's it.
Declarations might look like this: Declarations might look like this:
@ -485,17 +484,16 @@ case:
case_name: case_name:
- outline: <outline ref> - outline: <outline ref>
extrude: num # default = 1 extrude: num # default = 1
chamfer: [num_top, num_bottom] # default = [0, 0]
translate: [x, y, z] # default = [0, 0, 0] translate: [x, y, z] # default = [0, 0, 0]
rotate: [ax, ay, az] # default = [0, 0, 0] rotate: [ax, ay, az] # default = [0, 0, 0]
op: add|sub|diff # default = add op: add | sub | diff # default = add
- ... - ...
... ...
``` ```
`outline` specifies which outline to import onto the xy plane, while `extrude` specifies how much it should be extruded along the z axis. `outline` specifies which outline to import onto the xy plane, while `extrude` specifies how much it should be extruded along the z axis.
After the camfer to the top and/or bottom outside edges, the object is `translate`d/`rotate`d, and combined with what we have so far according to `op`. After that, the object is `translate`d, `rotate`d, and combined with what we have so far according to `op`.
If we only want to use an object as a building block for further objects, we can employ the same "start with an underscore" trick we learned at the outlines section. If we only want to use an object as a building block for further objects, we can employ the same "start with an underscore" trick we learned at the outlines section to make it "private".

View file

@ -11,7 +11,7 @@
"bin": "./src/cli.js", "bin": "./src/cli.js",
"scripts": { "scripts": {
"build": "rollup -c", "build": "rollup -c",
"test": "nyc mocha -r chai/register-should" "test": "nyc --reporter=html --reporter=text mocha -r chai/register-should"
}, },
"dependencies": { "dependencies": {
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
@ -28,7 +28,11 @@
}, },
"nyc": { "nyc": {
"all": true, "all": true,
"include": ["src/*.js"], "include": [
"exclude": ["src/cli.js"] "src/*.js"
],
"exclude": [
"src/cli.js"
]
} }
} }

View file

@ -1,11 +1,28 @@
#!/usr/bin/env node
const m = require('makerjs')
const fs = require('fs-extra') const fs = require('fs-extra')
const path = require('path') const path = require('path')
const yaml = require('js-yaml') const yaml = require('js-yaml')
const yargs = require('yargs') const yargs = require('yargs')
const points_lib = require('../helpers/points') const u = require('./utils')
const points_lib = require('./points')
const outline_lib = require('./outline') const outline_lib = require('./outline')
const dump_model = (model, file='model') => {
const assembly = m.model.originate({
models: u.deepcopy(model),
units: 'mm'
})
fs.mkdirpSync(path.dirname(`${file}.dxf`))
fs.writeFileSync(`${file}.dxf`, m.exporter.toDXF(assembly))
if (args.debug) {
fs.writeJSONSync(`${file}.json`, assembly, {spaces: 4})
}
}
const args = yargs const args = yargs
.option('config', { .option('config', {
alias: 'c', alias: 'c',
@ -24,48 +41,20 @@ const args = yargs
hidden: true, hidden: true,
type: 'boolean' type: 'boolean'
}) })
.option('outline', {
default: true,
describe: 'Generate 2D outlines',
type: 'boolean'
})
.option('pcb', {
default: false,
describe: 'Generate PCB draft',
type: 'boolean'
})
.option('case', {
default: false,
describe: 'Generate case files',
type: 'boolean'
})
.argv .argv
if (!args.outline && !args.pcb && !args.case) { fs.mkdirpSync(args.o)
yargs.showHelp('log')
console.log('Nothing to do...')
process.exit(0)
}
const config = yaml.load(fs.readFileSync(args.c).toString()) const config_parser = args.c.endsWith('.yaml') ? yaml.load : JSON.parse
const points = points_lib.parse(config) const config = config_parser(fs.readFileSync(args.c).toString())
const points = points_lib.parse(config.points)
if (args.debug) { if (args.debug) {
points_lib.dump(points) fs.writeJSONSync(path.join(args.o, 'points.json'), points, {spaces: 4})
} const size = 14
const rect = u.rect(size, size, [-size/2, -size/2])
if (args.outline) { const points_demo = outline_lib.layout(points, rect)
outline_lib.draw(points, config) dump_model(points_demo, path.join(args.o, 'points_demo'))
} }
console.log('Done.') console.log('Done.')
// exports.dump_model = (model, file='model', json=false) => {
// const assembly = m.model.originate({
// models: deepcopy(model),
// units: 'mm'
// })
// if (json) fs.writeFileSync(`${file}.json`, JSON.stringify(assembly, null, ' '))
// fs.writeFileSync(`${file}.dxf`, m.exporter.toDXF(assembly))
// }

View file

@ -1,23 +1,25 @@
const m = require('makerjs') const m = require('makerjs')
const fs = require('fs-extra')
const assert = require('assert').strict
const u = require('./utils') const u = require('./utils')
const layout = exports.layout = (points, shape) => {
const shapes = {}
for (const [pname, p] of Object.entries(points)) {
shapes[pname] = p.position(u.deepcopy(shape))
}
return {layout: {models: shapes}}
}
const outline = exports._outline = (points, config={}) => params => {
const outline = (points, config) => { let size = params.size || [18, 18]
if (!Array.isArray(size)) size = [size, size]
const corner = params.corner || 0
assert.ok(config.outline) const global_bind = config.bind || 5
const footprint = config.outline.footprint || 18
const corner = config.outline.corner || 0
const global_bind = config.outline.bind || 5
let glue = {paths: {}} let glue = {paths: {}}
if (config.outline.glue) { if (config.glue) {
const glue_conf = config.outline.glue
const internal_part = (line) => { const internal_part = (line) => {
// taking the middle part only, so that we don't interfere with corner rounding // taking the middle part only, so that we don't interfere with corner rounding
@ -25,23 +27,33 @@ const outline = (points, config) => {
} }
const get_line = (def={}) => { const get_line = (def={}) => {
const point = points[def.key] const ref = points[def.ref]
if (!point) throw new Error(`Point ${def.key} not found...`) if (!ref) throw new Error(`Point ${def.ref} not found...`)
let from = [0, 0]
let to = [ref.meta.mirrored ? -1 : 1, 0]
// todo: position according to point to get the lines...
let point = ref.clone().shift(def.shift || [0, 0])
point.rotate(def.rotate || 0, point.add(def.origin || [0, 0]))
const rect = m.model.originate(point.rect(footprint)) const rect = m.model.originate(point.rect(footprint))
line = rect.paths[def.line || 'top'] line = rect.paths[def.line || 'top']
return internal_part(line) return internal_part(line)
} }
assert.ok(glue_conf.top) assert.ok(config.glue.top)
const tll = get_line(glue_conf.top.left) const tll = get_line(config.glue.top.left)
const trl = get_line(glue_conf.top.right) const trl = get_line(config.glue.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
assert.ok(glue_conf.bottom) assert.ok(config.glue.bottom)
const bll = get_line(glue_conf.bottom.left) const bll = get_line(config.glue.bottom.left)
const brl = get_line(glue_conf.bottom.right) const brl = get_line(config.glue.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
@ -49,7 +61,7 @@ const outline = (points, config) => {
const left_waypoints = [] const left_waypoints = []
const right_waypoints = [] const right_waypoints = []
for (const w of glue_conf.waypoints || []) { for (const w of config.glue.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])

View file

@ -1,7 +1,7 @@
const m = require('makerjs') const m = require('makerjs')
const u = require('./utils') const u = require('./utils')
class Point { module.exports = class Point {
constructor(x=0, y=0, r=0, meta={}) { constructor(x=0, y=0, r=0, meta={}) {
if (Array.isArray(x)) { if (Array.isArray(x)) {
this.x = x[0] this.x = x[0]
@ -67,5 +67,3 @@ class Point {
return this.position(rect) return this.position(rect)
} }
} }
module.exports = Point

View file

@ -1,7 +1,37 @@
const m = require('makerjs') const m = require('makerjs')
const u = require('./utils') const u = require('./utils')
const Point = require('./point')
const push_rotation = (list, angle, origin) => { const extend_pair = exports._extend_pair = (to, from) => {
const to_type = u.type(to)
const from_type = u.type(from)
if (!from && ['array', 'object'].includes(to_type)) return to
if (to_type != from_type) return from
if (from_type == 'object') {
const res = {}
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) => {
let candidate = origin let candidate = origin
for (const r of list) { for (const r of list) {
candidate = m.point.rotate(candidate, r.angle, r.origin) candidate = m.point.rotate(candidate, r.angle, r.origin)
@ -12,9 +42,8 @@ const push_rotation = (list, angle, origin) => {
}) })
} }
const render_zone = (cols, rows, anchor=new Point(), reverse=false) => { const render_zone = exports._render_zone = (cols, rows, zone_wide_key, anchor) => {
const sign = reverse ? -1 : 1
const points = {} const points = {}
const rotations = [] const rotations = []
@ -24,7 +53,7 @@ const render_zone = (cols, rows, anchor=new Point(), reverse=false) => {
origin: anchor.p origin: anchor.p
}) })
for (const col of cols) { for (const [colname, col] of Object.entries(cols)) {
anchor.y += col.stagger || 0 anchor.y += col.stagger || 0
const col_anchor = anchor.clone() const col_anchor = anchor.clone()
@ -33,26 +62,36 @@ const render_zone = (cols, rows, anchor=new Point(), reverse=false) => {
col_anchor.r = 0 col_anchor.r = 0
// combine row data from zone-wide defs and col-specific defs // combine row data from zone-wide defs and col-specific defs
const col_specific = col.rows || [] const col_specific_rows = col.rows || {}
const zone_wide = rows || [] const zone_wide_rows = rows || {}
const actual_rows = [] const actual_rows = col_specific_rows || zone_wide_rows
for (let i = 0; i < zone_wide.length && i < col_specific.length; ++i) {
actual_rows.push(Object.assign({}, zone_wide[i], col_specific[i])) // get key config through the 4-level extension
const keys = []
for (const row of Object.keys(actual_rows)) {
const key = extend(zone_wide_key, col.key || {}, zone_wide_rows[row] || {}, col_specific_rows[row] || {})
key.col = col
key.row = row
key.name = `${colname}_${row}`
keys.push(key)
} }
for (const row of actual_rows) { // lay out keys
for (const key of keys) {
let point = col_anchor.clone() let point = col_anchor.clone()
for (const r of rotations) { for (const r of rotations) {
point.rotate(r.angle, r.origin) point.rotate(r.angle, r.origin)
} }
point.r += col.angle || 0 if (key.rotate) {
const name = `${col.name}_${row.name}` point.rotate(key.rotate, point.add(key.origin || [0, 0]).p)
point.meta = {col, row, name} }
points[name] = point point.meta = key
points[key.name] = point
col_anchor.y += row.padding || 19 col_anchor.y += key.padding || 19
} }
// apply col-level rotation for the next columns
if (col.rotate) { if (col.rotate) {
push_rotation( push_rotation(
rotations, rotations,
@ -61,23 +100,23 @@ const render_zone = (cols, rows, anchor=new Point(), reverse=false) => {
) )
} }
anchor.x += sign * (col.padding || 19) anchor.x += col.spread || 19
} }
return points return points
} }
const anchor = (raw, points={}) => { const anchor = exports._anchor = (raw, points={}) => {
let a = new Point() let a = new Point()
if (raw) { if (raw) {
if (raw.ref && points[raw.ref]) { if (raw.ref && points[raw.ref]) {
a = points[raw.ref].clone() a = points[raw.ref].clone()
} }
if (raw.shift) { if (raw.shift) {
a.x += raw.shift[0] a.x += raw.shift[0] || 0
a.y += raw.shift[1] a.y += raw.shift[1] || 0
} }
a.r += raw.angle || 0 a.r += raw.rotate || 0
} }
return a return a
} }
@ -90,29 +129,32 @@ exports.parse = (config) => {
points = Object.assign(points, render_zone( points = Object.assign(points, render_zone(
zone.columns || [], zone.columns || [],
zone.rows || [{name: 'default'}], zone.rows || [{name: 'default'}],
anchor(zone.anchor, points), zone.key || {},
!!zone.reverse anchor(zone.anchor, points)
)) ))
} }
if (config.angle) { if (config.rotate) {
for (const p of Object.values(points)) { for (const p of Object.values(points)) {
p.rotate(config.angle) p.rotate(config.rotate)
} }
} }
if (config.mirror) { if (config.mirror) {
let axis = anchor(config.mirror, points).x let axis = config.mirror.axis
if (!axis) {
axis = anchor(config.mirror, points).x
axis += (config.mirror.distance || 0) / 2 axis += (config.mirror.distance || 0) / 2
}
const mirrored_points = {} const mirrored_points = {}
for (const [name, p] of Object.entries(points)) { for (const [name, p] of Object.entries(points)) {
if (p.meta.col.asym == 'left' || p.meta.row.asym == 'left') continue if (p.meta.asym == 'left') continue
const mp = p.clone().mirror(axis) const mp = p.clone().mirror(axis)
mp.meta.mirrored = true mp.meta.mirrored = true
delete mp.meta.asym delete mp.meta.asym
mirrored_points[`mirror_${name}`] = mp mirrored_points[`mirror_${name}`] = mp
if (p.meta.col.asym == 'right' || p.meta.row.asym == 'right') { if (p.meta.asym == 'right') {
p.meta.col.skip = true p.meta.skip = true
} }
} }
Object.assign(points, mirrored_points) Object.assign(points, mirrored_points)
@ -120,7 +162,7 @@ exports.parse = (config) => {
const filtered = {} const filtered = {}
for (const [k, p] of Object.entries(points)) { for (const [k, p] of Object.entries(points)) {
if (p.meta.col.skip || p.meta.row.skip) continue if (p.meta.skip) continue
filtered[k] = p filtered[k] = p
} }

View file

@ -1,7 +1,19 @@
const m = require('makerjs') const m = require('makerjs')
exports.assert = (exp, msg) => {
if (!exp) {
throw new Error(msg)
}
}
exports.deepcopy = (value) => JSON.parse(JSON.stringify(value)) exports.deepcopy = (value) => JSON.parse(JSON.stringify(value))
exports.type = (val) => {
if (Array.isArray(val)) return 'array'
if (val === null) return 'null'
return typeof val
}
const eq = exports.eq = (a=[], b=[]) => { const eq = exports.eq = (a=[], b=[]) => {
return a[0] === b[0] && a[1] === b[1] return a[0] === b[0] && a[1] === b[1]
} }

17
test/complex.js Normal file
View file

@ -0,0 +1,17 @@
const fs = require('fs-extra')
const path = require('path')
const yaml = require('js-yaml')
const points_lib = require('../src/points')
const fixtures = path.join(__dirname, 'fixtures')
const absolem_config = yaml.load(fs.readFileSync(path.join(fixtures, 'absolem.yaml')).toString())
describe('Absolem', function() {
it('#points', function() {
const expected = fs.readJSONSync(path.join(fixtures, 'absolem_points.json'))
const actual = points_lib.parse(absolem_config.points)
// remove metadata, so that it only checks the points
Object.values(actual).map(val => delete val.meta)
actual.should.deep.equal(expected)
})
})

109
test/fixtures/absolem.yaml vendored Normal file
View file

@ -0,0 +1,109 @@
points:
zones:
matrix:
anchor:
rotate: 5
columns:
pinky:
rotate: -5
origin: [7, -7]
rows:
bottom:
home:
neighbors: [right]
top:
neighbors: [right]
ring:
stagger: 12
rows:
bottom:
neighbors: [left]
home:
neighbors: [right]
top:
neighbors: [right]
middle:
stagger: 5
rows:
bottom:
neighbors: [both]
home:
neighbors: [both]
top:
index:
stagger: -6
rows:
bottom:
neighbors: [right]
home:
neighbors: [left]
top:
neighbors: [left]
inner:
stagger: -2
rows:
bottom:
home:
neighbors: [left]
top:
neighbors: [left]
rows:
bottom:
neighbors: [,up]
home:
neighbors: [,up]
top:
thumbfan:
anchor:
ref: inner_bottom
shift: [-7, -19]
columns:
near:
spread: 21.25
rotate: -28
origin: [9.5, -9]
rows:
thumb:
neighbors: [right]
home:
spread: 21.25
rotate: -28
origin: [11.75, -9]
rows:
thumb:
neighbors: [both]
far:
rows:
thumb:
neighbors: [left]
rows:
thumb:
neighbors: [,up]
rotate: -20
mirror:
ref: pinky_home
distance: 223.7529778
outline:
bind: 10
glue:
top:
left:
ref: inner_top
shift: [,7]
right:
ref: mirror_inner_top
shift: [,7]
bottom:
left:
ref: far_thumb
shift: [7]
rotate: 90
right:
ref: mirror_far_thumb
shift: [-7]
rotate: 90
waypoints:
- percent: 50
width: 100
- percent: 90
width: 50

182
test/fixtures/absolem_points.json vendored Normal file
View file

@ -0,0 +1,182 @@
{
"pinky_bottom": {
"x": 0,
"y": 0,
"r": -15
},
"pinky_home": {
"x": 4.9175619,
"y": 18.3525907,
"r": -15
},
"pinky_top": {
"x": 9.8351237,
"y": 36.7051814,
"r": -15
},
"ring_bottom": {
"x": 22.7244416,
"y": 5.176704,
"r": -20
},
"ring_home": {
"x": 29.2228244,
"y": 23.0308638,
"r": -20
},
"ring_top": {
"x": 35.7212071,
"y": 40.8850235,
"r": -20
},
"middle_bottom": {
"x": 42.2887022,
"y": 3.3767843,
"r": -20
},
"middle_home": {
"x": 48.7870849,
"y": 21.2309442,
"r": -20
},
"middle_top": {
"x": 55.2854676,
"y": 39.0851039,
"r": -20
},
"index_bottom": {
"x": 58.0907411,
"y": -8.7597541,
"r": -20
},
"index_home": {
"x": 64.5891238,
"y": 9.0944057,
"r": -20
},
"index_top": {
"x": 71.0875065,
"y": 26.9485655,
"r": -20
},
"inner_bottom": {
"x": 75.2608606,
"y": -17.1375221,
"r": -20
},
"inner_home": {
"x": 81.7592433,
"y": 0.7166377,
"r": -20
},
"inner_top": {
"x": 88.257626,
"y": 18.5707976,
"r": -20
},
"near_thumb": {
"x": 62.1846295,
"y": -32.5975409,
"r": -20
},
"home_thumb": {
"x": 82.5841162,
"y": -47.013742,
"r": -48
},
"far_thumb": {
"x": 94.7890169,
"y": -68.8083815,
"r": -76
},
"mirror_pinky_bottom": {
"x": 233.5881016,
"y": 0,
"r": 15
},
"mirror_pinky_home": {
"x": 228.67053969999998,
"y": 18.3525907,
"r": 15
},
"mirror_pinky_top": {
"x": 223.7529779,
"y": 36.7051814,
"r": 15
},
"mirror_ring_bottom": {
"x": 210.86365999999998,
"y": 5.176704,
"r": 20
},
"mirror_ring_home": {
"x": 204.36527719999998,
"y": 23.0308638,
"r": 20
},
"mirror_ring_top": {
"x": 197.8668945,
"y": 40.8850235,
"r": 20
},
"mirror_middle_bottom": {
"x": 191.29939939999997,
"y": 3.3767843,
"r": 20
},
"mirror_middle_home": {
"x": 184.8010167,
"y": 21.2309442,
"r": 20
},
"mirror_middle_top": {
"x": 178.30263399999998,
"y": 39.0851039,
"r": 20
},
"mirror_index_bottom": {
"x": 175.49736049999998,
"y": -8.7597541,
"r": 20
},
"mirror_index_home": {
"x": 168.99897779999998,
"y": 9.0944057,
"r": 20
},
"mirror_index_top": {
"x": 162.5005951,
"y": 26.9485655,
"r": 20
},
"mirror_inner_bottom": {
"x": 158.327241,
"y": -17.1375221,
"r": 20
},
"mirror_inner_home": {
"x": 151.82885829999998,
"y": 0.7166377,
"r": 20
},
"mirror_inner_top": {
"x": 145.3304756,
"y": 18.5707976,
"r": 20
},
"mirror_near_thumb": {
"x": 171.4034721,
"y": -32.5975409,
"r": 20
},
"mirror_home_thumb": {
"x": 151.00398539999998,
"y": -47.013742,
"r": 48
},
"mirror_far_thumb": {
"x": 138.79908469999998,
"y": -68.8083815,
"r": 76
}
}