Skip to content

Commit 50f45d8

Browse files
Olson3Rrianadon
andauthored
Add keyboard layout selector (QWERTY/Colemak/Colemak-DH/Dvorak/Workman) (#87)
Co-authored-by: Ryan Adolf <ryanadolf123@gmail.com>
1 parent ae44f71 commit 50f45d8

41 files changed

Lines changed: 2761 additions & 177 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Makefile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,8 @@ npm-install:
8383
fetch-assets:
8484
src/scripts/fetch-assets.sh
8585
fetch-history:
86-
git fetch origin main:refs/remotes/origin/main \
87-
$$VERCEL_GIT_COMMIT_REF:refs/remotes/origin/$$VERCEL_GIT_COMMIT_REF \
88-
--depth=100
86+
git fetch origin main:refs/remotes/origin/main --depth=100 && git fetch origin \
87+
$$VERCEL_GIT_COMMIT_REF:refs/remotes/origin/$$VERCEL_GIT_COMMIT_REF --depth=100 || true
8988
ci-base: build keycaps-simple2 keycaps2 parts parts-simple
9089
ci: fetch-history fetch-assets ci-setup build parts vite-build docs-ci
9190
quickstart: npm-install ci-setup ci-base

src/lib/3d/Keyboard.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@
190190
{@const index = nthIndex($protoConfig, side, key.i)}
191191
<KeyboardKey
192192
{index}
193+
{side}
193194
visible={visible && (!keyColor || keyColor[1] != 0)}
194195
position={pressedLetter && lett == pressedLetter
195196
? adjustedPosition(key, translation)
@@ -219,6 +220,7 @@
219220
{@const index = nthIndex($protoConfig, side, key.i)}
220221
<KeyboardKey
221222
{index}
223+
{side}
222224
visible={visible &&
223225
(key.key.type != 'blank' || !($noBlanks || keyColor)) &&
224226
(!PART_INFO[key.key.type].keycap || !switchColor || switchColor[1] != 0)}
@@ -259,6 +261,6 @@
259261
260262
{#each keys as key}
261263
{@const index = nthIndex($protoConfig, side, key.i)}
262-
<KeyboardKeyInstance {index} brightness={0.7} position={key.pos} rotation={key.rot} />
264+
<KeyboardKeyInstance {index} {side} brightness={0.7} position={key.pos} rotation={key.rot} />
263265
{/each}
264266
</InstancedMesh> -->

src/lib/3d/KeyboardKey.svelte

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<script lang="ts">
22
import { T } from '@threlte/core'
3-
import { hoveredKey, clickedKey } from '$lib/store'
3+
import { hoveredKey, clickedKey, clickedSide } from '$lib/store'
44
import { getContext } from 'svelte'
55
import type { interactivity } from '@threlte/extras'
66
77
export let index: number | null
8+
export let side: 'left' | 'right' | 'unibody' = 'right'
89
910
type InteractivityContext = ReturnType<typeof interactivity>
1011
const context: InteractivityContext = getContext('interactivity')
@@ -14,9 +15,15 @@
1415
{...$$restProps}
1516
on:pointerenter={() => ($hoveredKey = index)}
1617
on:pointerleave={() => ($hoveredKey = null)}
17-
on:click={() => ($clickedKey = index)}
18+
on:click={() => {
19+
$clickedKey = index
20+
$clickedSide = side
21+
}}
1822
on:pointermissed={() => {
19-
if (!context.initialHits.length) $clickedKey = null
23+
if (!context.initialHits.length) {
24+
$clickedKey = null
25+
$clickedSide = null
26+
}
2027
}}
2128
>
2229
<slot />

src/lib/3d/KeyboardKeyInstance.svelte

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { hoveredKey, clickedKey, selectMode, protoConfig } from '$lib/store'
2+
import { hoveredKey, clickedKey, clickedSide, selectMode, protoConfig } from '$lib/store'
33
import { getContext } from 'svelte'
44
import { nthKey, type CosmosKeyboard } from '$lib/worker/config.cosmos'
55
import { Instance, interactivity } from '@threlte/extras'
@@ -9,6 +9,7 @@
99
export let index: number | null
1010
export let position: Vector3Tuple
1111
export let rotation: Vector4Tuple
12+
export let side: 'left' | 'right' | 'unibody' = 'right'
1213
1314
type InteractivityContext = ReturnType<typeof interactivity>
1415
const context: InteractivityContext = getContext('interactivity')
@@ -41,8 +42,14 @@
4142
quaternion={rotation}
4243
on:pointerenter={() => ($hoveredKey = index)}
4344
on:pointerleave={() => ($hoveredKey = null)}
44-
on:click={() => ($clickedKey = index)}
45+
on:click={() => {
46+
$clickedKey = index
47+
$clickedSide = side
48+
}}
4549
on:pointermissed={() => {
46-
if (!context.initialHits.length) $clickedKey = null
50+
if (!context.initialHits.length) {
51+
$clickedKey = null
52+
$clickedSide = null
53+
}
4754
}}
4855
/>

src/lib/bcp.test.ts

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
/**
2+
* Tests for BCP 47 library.
3+
*
4+
* Adapted from https://github.com/wooorm/bcp-47
5+
*
6+
* MIT license:
7+
* Copyright (c) 2016 Titus Wormer <tituswormer@gmail.com>
8+
*
9+
* Permission is hereby granted, free of charge, to any person obtaining
10+
* a copy of this software and associated documentation files (the
11+
* 'Software'), to deal in the Software without restriction, including
12+
* without limitation the rights to use, copy, modify, merge, publish,
13+
* distribute, sublicense, and/or sell copies of the Software, and to
14+
* permit persons to whom the Software is furnished to do so, subject to
15+
* the following conditions:
16+
*
17+
* The above copyright notice and this permission notice shall be
18+
* included in all copies or substantial portions of the Software.
19+
*
20+
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
21+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22+
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23+
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24+
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
25+
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
26+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27+
*/
28+
29+
import { expect, test } from 'bun:test'
30+
import { parseBCP } from './bcp'
31+
32+
test('.parse()', () => {
33+
expect(parseBCP('i-klingon')).toEqual({
34+
language: 'tlh',
35+
extendedLanguageSubtags: [],
36+
script: null,
37+
region: null,
38+
variants: [],
39+
extensions: [],
40+
privateuse: [],
41+
irregular: null,
42+
regular: null,
43+
})
44+
45+
expect(parseBCP('i-klingon', { normalize: false })).toEqual({
46+
language: null,
47+
extendedLanguageSubtags: [],
48+
script: null,
49+
region: null,
50+
variants: [],
51+
extensions: [],
52+
privateuse: [],
53+
irregular: 'i-klingon',
54+
regular: null,
55+
})
56+
57+
expect(parseBCP('i-default')).toEqual({
58+
language: null,
59+
extendedLanguageSubtags: [],
60+
script: null,
61+
region: null,
62+
variants: [],
63+
extensions: [],
64+
privateuse: [],
65+
irregular: 'i-default',
66+
regular: null,
67+
})
68+
69+
expect(parseBCP('zh-min')).toEqual({
70+
language: null,
71+
extendedLanguageSubtags: [],
72+
script: null,
73+
region: null,
74+
variants: [],
75+
extensions: [],
76+
privateuse: [],
77+
irregular: null,
78+
regular: 'zh-min',
79+
})
80+
})
81+
82+
test('Too long variant', () => {
83+
const fixture = 'en-GB-abcdefghi'
84+
85+
expect(parseBCP(fixture, { warning })).toEqual({
86+
language: null,
87+
extendedLanguageSubtags: [],
88+
script: null,
89+
region: null,
90+
variants: [],
91+
extensions: [],
92+
privateuse: [],
93+
irregular: null,
94+
regular: null,
95+
})
96+
97+
function warning() {
98+
expect(arguments[0]).toBe(
99+
'Too long variant, expected at most 8 characters',
100+
)
101+
expect(arguments[1]).toBe(1)
102+
expect(arguments[2]).toBe(14)
103+
expect(arguments.length).toBe(3)
104+
}
105+
106+
expect(parseBCP(fixture, { forgiving: true })).toEqual({
107+
language: 'en',
108+
extendedLanguageSubtags: [],
109+
script: null,
110+
region: 'GB',
111+
variants: [],
112+
extensions: [],
113+
privateuse: [],
114+
irregular: null,
115+
regular: null,
116+
})
117+
})
118+
119+
test('Too many subtags', () => {
120+
const fixture = 'aa-bbb-ccc-ddd-eee'
121+
122+
expect(parseBCP(fixture, { warning })).toEqual({
123+
language: null,
124+
extendedLanguageSubtags: [],
125+
script: null,
126+
region: null,
127+
variants: [],
128+
extensions: [],
129+
privateuse: [],
130+
irregular: null,
131+
regular: null,
132+
})
133+
134+
function warning() {
135+
expect(arguments[0]).toBe(
136+
'Too many extended language subtags, expected at most 3 subtags',
137+
)
138+
expect(arguments[1]).toBe(3)
139+
expect(arguments[2]).toBe(14)
140+
expect(arguments.length).toBe(3)
141+
}
142+
143+
expect(parseBCP(fixture, { forgiving: true })).toEqual({
144+
language: 'aa',
145+
extendedLanguageSubtags: ['bbb', 'ccc', 'ddd'],
146+
script: null,
147+
region: null,
148+
variants: [],
149+
extensions: [],
150+
privateuse: [],
151+
irregular: null,
152+
regular: null,
153+
})
154+
})
155+
156+
await test('Too long extension', () => {
157+
const fixture = 'en-i-abcdefghi'
158+
159+
expect(parseBCP(fixture, { warning })).toEqual({
160+
language: null,
161+
extendedLanguageSubtags: [],
162+
script: null,
163+
region: null,
164+
variants: [],
165+
extensions: [],
166+
privateuse: [],
167+
irregular: null,
168+
regular: null,
169+
})
170+
171+
function warning() {
172+
expect(arguments[0]).toBe(
173+
'Too long extension, expected at most 8 characters',
174+
)
175+
expect(arguments[1]).toBe(2)
176+
expect(arguments[2]).toBe(13)
177+
expect(arguments.length).toBe(3)
178+
}
179+
180+
expect(parseBCP(fixture, { forgiving: true })).toEqual({
181+
language: 'en',
182+
extendedLanguageSubtags: [],
183+
script: null,
184+
region: null,
185+
variants: [],
186+
extensions: [],
187+
privateuse: [],
188+
irregular: null,
189+
regular: null,
190+
})
191+
})
192+
193+
test('Empty extension', () => {
194+
const fixture = 'en-i-a'
195+
196+
expect(parseBCP(fixture, { warning })).toEqual({
197+
language: null,
198+
extendedLanguageSubtags: [],
199+
script: null,
200+
region: null,
201+
variants: [],
202+
extensions: [],
203+
privateuse: [],
204+
irregular: null,
205+
regular: null,
206+
})
207+
208+
function warning() {
209+
expect(arguments[0]).toBe(
210+
'Empty extension, extensions must have at least 2 characters of content',
211+
)
212+
expect(arguments[1]).toBe(4)
213+
expect(arguments[2]).toBe(4)
214+
expect(arguments.length).toBe(3)
215+
}
216+
217+
expect(parseBCP(fixture, { forgiving: true })).toEqual({
218+
language: 'en',
219+
extendedLanguageSubtags: [],
220+
script: null,
221+
region: null,
222+
variants: [],
223+
extensions: [],
224+
privateuse: [],
225+
irregular: null,
226+
regular: null,
227+
})
228+
})
229+
230+
test('Too long private-use', () => {
231+
const fixture = 'en-x-abcdefghi'
232+
233+
expect(parseBCP(fixture, { warning })).toEqual({
234+
language: null,
235+
extendedLanguageSubtags: [],
236+
script: null,
237+
region: null,
238+
variants: [],
239+
extensions: [],
240+
privateuse: [],
241+
irregular: null,
242+
regular: null,
243+
})
244+
245+
function warning() {
246+
expect(arguments[0]).toBe(
247+
'Too long private-use area, expected at most 8 characters',
248+
)
249+
expect(arguments[1]).toBe(5)
250+
expect(arguments[2]).toBe(13)
251+
expect(arguments.length).toBe(3)
252+
}
253+
254+
expect(parseBCP(fixture, { forgiving: true })).toEqual({
255+
language: 'en',
256+
extendedLanguageSubtags: [],
257+
script: null,
258+
region: null,
259+
variants: [],
260+
extensions: [],
261+
privateuse: [],
262+
irregular: null,
263+
regular: null,
264+
})
265+
})
266+
267+
test('Extra content', () => {
268+
const fixture = 'abcdefghijklmnopqrstuvwxyz'
269+
270+
expect(parseBCP(fixture, { warning })).toEqual({
271+
language: null,
272+
extendedLanguageSubtags: [],
273+
script: null,
274+
region: null,
275+
variants: [],
276+
extensions: [],
277+
privateuse: [],
278+
irregular: null,
279+
regular: null,
280+
})
281+
282+
function warning() {
283+
expect(arguments[0]).toBe('Found superfluous content after tag')
284+
expect(arguments[1]).toBe(6)
285+
expect(arguments[2]).toBe(0)
286+
expect(arguments.length).toBe(3)
287+
}
288+
289+
expect(parseBCP(fixture, { forgiving: true })).toEqual({
290+
language: null,
291+
extendedLanguageSubtags: [],
292+
script: null,
293+
region: null,
294+
variants: [],
295+
extensions: [],
296+
privateuse: [],
297+
irregular: null,
298+
regular: null,
299+
})
300+
})

0 commit comments

Comments
 (0)