From 06a76a37366fbd5526aba4201f1adb2ab7634efc Mon Sep 17 00:00:00 2001 From: Ryan Adolf Date: Wed, 6 May 2026 23:42:10 -0500 Subject: [PATCH 01/15] Center cluster initial progress --- src/lib/3d/Keyboard.svelte | 2 +- src/lib/3d/KeyboardModel.svelte | 2 +- src/lib/worker/api.ts | 4 +- src/lib/worker/check.ts | 14 +- src/lib/worker/config.cosmos.ts | 165 +++++++++++++----- src/lib/worker/config.serialize.ts | 32 +++- src/lib/worker/config.test.ts | 59 +++++++ src/lib/worker/config.ts | 59 +++++-- src/proto/cosmos.proto | 1 + src/proto/cosmosStructs.ts | 2 +- src/routes/beta/App.svelte | 14 +- src/routes/beta/lib/ConfError.svelte | 2 +- .../beta/lib/dialogs/AssemblyView.svelte | 2 +- .../beta/lib/dialogs/DownloadDialog.svelte | 12 +- src/routes/beta/lib/editor/PeaConfig.svelte | 10 ++ .../beta/lib/editor/VisualEditor2.svelte | 96 +++++++++- .../beta/lib/editor/visualEditorHelpers.ts | 160 ++++++++++++++++- src/routes/beta/lib/firmware/qmk.ts | 2 +- src/routes/beta/lib/modelGLTF.ts | 6 +- src/routes/beta/lib/viewers/Viewer3D.svelte | 2 +- .../beta/lib/viewers/viewer3dHelpers.ts | 4 +- 21 files changed, 556 insertions(+), 94 deletions(-) diff --git a/src/lib/3d/Keyboard.svelte b/src/lib/3d/Keyboard.svelte index dd4dd083..1826860a 100644 --- a/src/lib/3d/Keyboard.svelte +++ b/src/lib/3d/Keyboard.svelte @@ -31,7 +31,7 @@ export let translation: number = 0 export let flip = true export let reachability: boolean[] | undefined = undefined - export let side: 'left' | 'right' | 'unibody' + export let side: 'left' | 'right' | 'center' | 'unibody' export let keyColor: [any, number] | undefined = undefined export let trackballColor: any | undefined = undefined export let switchColor: [any, number] | undefined = undefined diff --git a/src/lib/3d/KeyboardModel.svelte b/src/lib/3d/KeyboardModel.svelte index fdb2ce98..25a00651 100644 --- a/src/lib/3d/KeyboardModel.svelte +++ b/src/lib/3d/KeyboardModel.svelte @@ -10,7 +10,7 @@ export let keebGeometry: Geometry | undefined = undefined export let meshes: KeyboardMeshes export let transparency: number = 100 - export let side: 'left' | 'right' | 'unibody' + export let side: 'left' | 'right' | 'center' | 'unibody' export let noWeb = false diff --git a/src/lib/worker/api.ts b/src/lib/worker/api.ts index 33ce4d94..835e5e39 100644 --- a/src/lib/worker/api.ts +++ b/src/lib/worker/api.ts @@ -306,7 +306,7 @@ async function getModel(conf: Cuttleform, name: string, stitchWalls: boolean, fl } } -export async function getSTL(conf: Cuttleform, name: string, side: 'left' | 'right' | 'unibody' | 'test', stitchWalls: boolean) { +export async function getSTL(conf: Cuttleform, name: string, side: 'left' | 'right' | 'center' | 'unibody' | 'test', stitchWalls: boolean) { const flip = side == 'left' let model = await getModel(conf, name, stitchWalls, flip) if (name == 'wristrest' && side == 'unibody' && conf.wristRestRight && model) { @@ -385,7 +385,7 @@ function meshWithVolumeAndSupport(solid: Solid, minZ: number) { // return props.Mass() // } -export async function intersections(conf: Cuttleform, side: 'left' | 'right' | 'unibody'): Promise { +export async function intersections(conf: Cuttleform, side: 'left' | 'right' | 'center' | 'unibody'): Promise { const intersections: ConfErrors = [] try { const geometry = newGeometry(conf) diff --git a/src/lib/worker/check.ts b/src/lib/worker/check.ts index 2f745d33..9097cf82 100644 --- a/src/lib/worker/check.ts +++ b/src/lib/worker/check.ts @@ -79,7 +79,7 @@ const PROPERTIES = ['aspect', 'type', 'position'] export type ConfError = & (IntersectionError | MissingError | WrongError | OobError | InvalidError | ExceptionError | NanError | NoKeysError | WallBoundsError | WrongFormatError | SamePositionError | NotEnoughPinsError) & { - side: 'left' | 'right' | 'unibody' + side: 'left' | 'right' | 'center' | 'unibody' } export type ConfErrors = ConfError[] @@ -98,7 +98,7 @@ export function salientError(e: ConfErrors): ConfError { return e[0] } -export function checkConfig(conf: Cuttleform, geometry: Geometry | undefined, check3d = true, side: 'left' | 'right' | 'unibody'): ConfErrors { +export function checkConfig(conf: Cuttleform, geometry: Geometry | undefined, check3d = true, side: 'left' | 'right' | 'center' | 'unibody'): ConfErrors { if (!conf.keys) return [{ type: 'wrongformat', side }] if (!conf.keys.length) return [{ type: 'nokeys', side }] @@ -270,7 +270,7 @@ function prismTriangles(facePoints: Trsf[], center: Trsf, depth: number, index: }) } -export function* keycapIntersections(conf: Cuttleform, trsfs: Trsf[], web: ITriangle[], side: 'left' | 'right' | 'unibody') { +export function* keycapIntersections(conf: Cuttleform, trsfs: Trsf[], web: ITriangle[], side: 'left' | 'right' | 'center' | 'unibody') { const tree = new Octree() for (const tri of web) { tree.addTriangle(tri) @@ -288,7 +288,7 @@ export function* keycapIntersections(conf: Cuttleform, trsfs: Trsf[], web: ITria yield* treeIntersections(conf, tree, 'keycap', side) } -export function* partIntersections(conf: Cuttleform, trsfs: Trsf[], side: 'left' | 'right' | 'unibody') { +export function* partIntersections(conf: Cuttleform, trsfs: Trsf[], side: 'left' | 'right' | 'center' | 'unibody') { const tree = new Octree() for (let i = 0; i < trsfs.length; i++) { const key = conf.keys[i] @@ -302,7 +302,7 @@ export function* partIntersections(conf: Cuttleform, trsfs: Trsf[], side: 'left' yield* treeIntersections(conf, tree, 'part', side) } -export function* unsortedSocketIntersections(conf: Cuttleform, trsfs: Trsf[], critPts: CriticalPoints[], web: ITriangle[], side: 'left' | 'right' | 'unibody') { +export function* unsortedSocketIntersections(conf: Cuttleform, trsfs: Trsf[], critPts: CriticalPoints[], web: ITriangle[], side: 'left' | 'right' | 'center' | 'unibody') { const tree = new Octree() for (const tri of web) { tree.addTriangle(tri) @@ -324,7 +324,7 @@ export function* unsortedSocketIntersections(conf: Cuttleform, trsfs: Trsf[], cr * Every socket-socket intersection will generate a socket->wall intersection as well, * but the socket-socket intersections are easier to debug so they should get priority. */ -export function* socketIntersections(conf: Cuttleform, trsfs: Trsf[], critPts: CriticalPoints[], web: ITriangle[], side: 'left' | 'right' | 'unibody') { +export function* socketIntersections(conf: Cuttleform, trsfs: Trsf[], critPts: CriticalPoints[], web: ITriangle[], side: 'left' | 'right' | 'center' | 'unibody') { const socketSocketIntersections: ConfError[] = [] const socketWallIntersections: ConfError[] = [] for (const intersection of unsortedSocketIntersections(conf, trsfs, critPts, web, side)) { @@ -343,7 +343,7 @@ function* treeIntersections( conf: Cuttleform, tree: Octree, what: 'keycap' | 'socket' | 'part', - side: 'left' | 'right' | 'unibody', + side: 'left' | 'right' | 'center' | 'unibody', ignoreTouching = false, intersected?: DefaultMap> | undefined, ): Generator { diff --git a/src/lib/worker/config.cosmos.ts b/src/lib/worker/config.cosmos.ts index 055c1f1d..ebf891f6 100644 --- a/src/lib/worker/config.cosmos.ts +++ b/src/lib/worker/config.cosmos.ts @@ -3,6 +3,10 @@ import ETrsf, { Constant, fullMirrorETrsf, type MatrixOptions, mirror } from '$l import { flippedKey } from '$lib/geometry/keycaps' import { decodeVariant, encodeVariant, PART_INFO, socketSize } from '$lib/geometry/socketsParts' import { type ClusterName, type ClusterSide, type ClusterType, type Connector, decodeClusterFlags, encodeClusterFlags, type PartVariant, type ScrewFlags } from '$target/cosmosStructs' + +/** Runtime side type — extends generated ClusterSide with 'center'. The 'center' + * value is never written to the bitfield; it rides on `name` instead. */ +export type CosmosClusterSide = ClusterSide | 'center' import type { Curvature } from '$target/proto/cosmos' import { Matrix4, Vector3 } from 'three' import { type AnyShell, curvature, type Cuttleform, type CuttleKey, decodeTuple, encodeTuple, type FullCuttleform, type Keycap, matrixToRPY, tupleToRot, tupleToXYZ } from './config' @@ -44,7 +48,7 @@ type CosmosCurvature = Curvature export type CosmosCluster = { name: ClusterName - side: ClusterSide + side: CosmosClusterSide type: ClusterType curvature: CosmosCurvature profile: Keycap['profile'] | undefined @@ -107,6 +111,7 @@ export type CosmosKeyboard = wristRestPosition: bigint connectorLeftIndex: number connectorRightIndex: number + connectorCenterIndex: number connectors: ConnectorMaybeCustom[] mirrorConnectors: boolean plate: Cuttleform['plate'] @@ -147,6 +152,7 @@ export function getRowColumn(t: ETrsf) { export function sortClusters(clusters: CosmosCluster[]) { const val = (c: CosmosCluster) => { let v = 0 + if (c.side == 'center') v += 5 if (c.side == 'left') v += 10 if (c.name == 'thumbs') v += 1 return v @@ -154,7 +160,12 @@ export function sortClusters(clusters: CosmosCluster[]) { return clusters.sort((a, b) => val(a) - val(b)) } -function toCosmosClusters(keys: CuttleKey[], side: 'left' | 'right', globalProfile: Keycap['profile'], globalPartType: PartType, globalCurvature: Curvature, wrOriginInv: Trsf) { +/** True if the keyboard has a populated center cluster. */ +export function hasCenter(c: CosmosKeyboard): boolean { + return c.clusters.some(cl => cl.side === 'center' && cl.clusters.length > 0) +} + +function toCosmosClusters(keys: CuttleKey[], side: CosmosClusterSide, globalProfile: Keycap['profile'], globalPartType: PartType, globalCurvature: Curvature, wrOriginInv: Trsf) { const clusters: CosmosCluster[] = [] for (const [name, trsfGrp] of collectByClusterName(keys).entries()) { @@ -162,6 +173,8 @@ function toCosmosClusters(keys: CuttleKey[], side: 'left' | 'right', globalProfi const clusterProfile = dominantProfile(trsfGrp) ?? globalProfile const clusterPartType = decodePartType(dominantPartType(trsfGrp) ?? encodePartType(globalPartType)) const clusterType = decodeClusterFlags(dominantClusterType(trsfGrp, side)) + // Restore the runtime side='center' after decoding (encode wrote bit-side='right'). + if (side === 'center') (clusterType as any).side = 'center' const clusterTrsf = getTransformBy(trsfGrp[0])! const cluster: CosmosCluster = { ...clusterType, @@ -182,6 +195,7 @@ function toCosmosClusters(keys: CuttleKey[], side: 'left' | 'right', globalProfi const columnPartType = decodePartType(dominantPartType(colGrp) ?? encodePartType(clusterPartType)) const columnStagger = dominantStagger(colGrp) const columnType = decodeClusterFlags(dominantClusterType(colGrp, side)) + if (side === 'center') (columnType as any).side = 'center' let columnTrsf = new Trsf().fromMatrix(columnStagger.split(',').map(Number)) if (columnStagger.startsWith('!')) { columnTrsf = new Trsf().fromMatrix(columnStagger.slice(1).split(',').map(Number)) @@ -253,8 +267,12 @@ function toCosmosClusters(keys: CuttleKey[], side: 'left' | 'right', globalProfi position: undefined, rotation: undefined, } - if (!clusters.find(c => c.name == 'fingers')) clusters.push({ ...defaultCluster, name: 'fingers' }) - if (!clusters.find(c => c.name == 'thumbs')) clusters.push({ ...defaultCluster, name: 'thumbs' }) + if (side === 'center') { + if (!clusters.find(c => c.name == 'center')) clusters.push({ ...defaultCluster, name: 'center' }) + } else { + if (!clusters.find(c => c.name == 'fingers')) clusters.push({ ...defaultCluster, name: 'fingers' }) + if (!clusters.find(c => c.name == 'thumbs')) clusters.push({ ...defaultCluster, name: 'thumbs' }) + } return clusters } @@ -286,6 +304,7 @@ export function toFullCosmosConfig(conf: FullCuttleform, flipLeft = false): Cosm kbd.clusters.push(...c.clusters) if (side == 'left') kbd.connectorLeftIndex = c.connectorLeftIndex if (side == 'right') kbd.connectorRightIndex = c.connectorRightIndex + if (side == 'center') kbd.connectorCenterIndex = c.connectorCenterIndex } } if (!kbd) throw new Error('No configuration for keyboard') @@ -309,7 +328,7 @@ export function toFullCosmosConfig(conf: FullCuttleform, flipLeft = false): Cosm return kbd } -export function toCosmosConfig(conf: Cuttleform, side: 'left' | 'right' | 'unibody', overrideWristRest: boolean, flipLeft = false): CosmosKeyboard { +export function toCosmosConfig(conf: Cuttleform, side: 'left' | 'right' | 'center' | 'unibody', overrideWristRest: boolean, flipLeft = false): CosmosKeyboard { const globalCurvature = dominantCurvature(conf.keys) const globalProfile = dominantProfile(conf.keys) ?? 'xda' const globalPartType = decodePartType(dominantPartType(conf.keys) ?? encodePartType({ type: 'mx-better', aspect: 1 })) @@ -343,8 +362,9 @@ export function toCosmosConfig(conf: Cuttleform, side: 'left' | 'right' | 'unibo shell: conf.shell, plate: conf.plate, wristRestEnable: !!conf.wristRestRight, - connectorLeftIndex: conf.connectorIndex, - connectorRightIndex: conf.connectorIndex, + connectorLeftIndex: side === 'center' ? -1 : conf.connectorIndex, + connectorRightIndex: side === 'center' ? -1 : conf.connectorIndex, + connectorCenterIndex: side === 'center' ? conf.connectorIndex : -1, unibody: side == 'unibody', wristRestProps: conf.wristRestRight ? { @@ -369,10 +389,17 @@ export function toCosmosConfig(conf: Cuttleform, side: 'left' | 'right' | 'unibo }, wristRestPosition: overrideWristRest ? KEYBOARD_DEFAULTS.wristRestPosition! : encodeTuple(wrOrigin.xyz().map(t => Math.round(t * 10))), clusters: side == 'unibody' - ? [ - ...toCosmosClusters(filterUnibodySide(conf.keys, 'right'), 'right', globalProfile, globalPartType, globalCurvature, wrOriginInv), - ...toCosmosClusters(filterUnibodySide(conf.keys, 'left'), 'left', globalProfile, globalPartType, globalCurvature, flippedWrOriginInv), - ] + ? (() => { + const centerKeys = conf.keys.filter(k => k.cluster === 'center') + const result = [ + ...toCosmosClusters(filterUnibodySide(conf.keys, 'right'), 'right', globalProfile, globalPartType, globalCurvature, wrOriginInv), + ...toCosmosClusters(filterUnibodySide(conf.keys, 'left'), 'left', globalProfile, globalPartType, globalCurvature, flippedWrOriginInv), + ] + if (centerKeys.length) { + result.push(...toCosmosClusters(centerKeys, 'center', globalProfile, globalPartType, globalCurvature, wrOriginInv)) + } + return result + })() : toCosmosClusters(conf.keys, side, globalProfile, globalPartType, globalCurvature, wrOriginInv), } if (flipLeft && side == 'left') keyboard.clusters = keyboard.clusters.map(c => mirrorCluster(c, false)) @@ -380,11 +407,12 @@ export function toCosmosConfig(conf: Cuttleform, side: 'left' | 'right' | 'unibo return keyboard } -/** Estimate which side of the keyboard a key is on */ +/** Estimate which side of the keyboard a key is on. Center-cluster keys are excluded — they're handled separately. */ function filterUnibodySide(keys: CuttleKey[], side: 'left' | 'right') { - const xs = keys.map(k => k.position.evaluate({ flat: false }).origin().x) + const sideKeys = keys.filter(k => k.cluster !== 'center') + const xs = sideKeys.map(k => k.position.evaluate({ flat: false }).origin().x) const center = sum(xs) / xs.length - return keys.filter((k, i) => side == 'left' ? (xs[i] < center) : (xs[i] > center)) + return sideKeys.filter((k, i) => side == 'left' ? (xs[i] < center) : (xs[i] > center)) } function dominantCurvature(keys: CuttleKey[]): CosmosKeyboard['curvature'] { @@ -428,8 +456,14 @@ function dominantCurvature(keys: CuttleKey[]): CosmosKeyboard['curvature'] { } } -export function sideFromCosmosConfig(c: CosmosKeyboard, side: 'left' | 'right' | 'unibody', flipLeft = true): Cuttleform | undefined { +export function sideFromCosmosConfig(c: CosmosKeyboard, side: 'left' | 'right' | 'center' | 'unibody', flipLeft = true): Cuttleform | undefined { const wrPos = decodeTuple(c.wristRestPosition) + const isCenter = side == 'center' + const connectorIndex = side == 'left' + ? c.connectorLeftIndex + : side == 'center' + ? c.connectorCenterIndex + : c.connectorRightIndex const conf: Cuttleform = { wallThickness: c.wallThickness, wallShrouding: c.wallShrouding, @@ -448,12 +482,13 @@ export function sideFromCosmosConfig(c: CosmosKeyboard, side: 'left' | 'right' | clearScrews: c.clearScrews, rounded: JSON.parse(JSON.stringify(c.rounded)), connectors: c.connectors, - connectorIndex: side == 'left' ? c.connectorLeftIndex : c.connectorRightIndex, + connectorIndex, microcontroller: c.microcontroller, microcontrollerAngle: c.microcontrollerAngle, fastenMicrocontroller: c.fastenMicrocontroller, flipConnectors: side == 'left' && !c.mirrorConnectors, - wristRestLeft: c.wristRestEnable + // Center piece never has a wrist rest. + wristRestLeft: !isCenter && c.wristRestEnable ? { angle: c.wristRestProps.angle, taper: c.wristRestProps.taper, @@ -464,7 +499,7 @@ export function sideFromCosmosConfig(c: CosmosKeyboard, side: 'left' | 'right' | extension: c.wristRestProps.extensionLeft, } : undefined, - wristRestRight: c.wristRestEnable + wristRestRight: !isCenter && c.wristRestEnable ? { angle: c.wristRestProps.angle, taper: c.wristRestProps.taper, @@ -479,11 +514,29 @@ export function sideFromCosmosConfig(c: CosmosKeyboard, side: 'left' | 'right' | shell: c.shell, plate: c.plate, } - const clusters: CosmosCluster[] = c.clusters.filter(c => side == 'unibody' || c.side == side) - if (side == 'left' && !c.clusters.find(c => c.side == 'left' && c.name == 'fingers')) clusters.unshift(mirrorCluster(c.clusters.find(c => c.side == 'right' && c.name == 'fingers')!)) - if (side == 'unibody' && !c.clusters.find(c => c.side == 'left' && c.name == 'fingers')) clusters.splice(2, 0, mirrorCluster(c.clusters.find(c => c.side == 'right' && c.name == 'fingers')!)) - if (side != 'right' && !c.clusters.find(c => c.side == 'left' && c.name == 'thumbs')) clusters.push(mirrorCluster(c.clusters.find(c => c.side == 'right' && c.name == 'thumbs')!)) - // console.log('CLUSTERS', clusters) + let clusters: CosmosCluster[] + if (side == 'center') { + // Center piece: only the center cluster's keys. + clusters = c.clusters.filter(cl => cl.side === 'center') + } else if (side == 'unibody') { + // Unibody: include both halves and center keys. + clusters = [...c.clusters] + if (!c.clusters.find(cl => cl.side == 'left' && cl.name == 'fingers')) { + clusters.splice(2, 0, mirrorCluster(c.clusters.find(cl => cl.side == 'right' && cl.name == 'fingers')!)) + } + if (!c.clusters.find(cl => cl.side == 'left' && cl.name == 'thumbs')) { + clusters.push(mirrorCluster(c.clusters.find(cl => cl.side == 'right' && cl.name == 'thumbs')!)) + } + } else { + // Split left or right. + clusters = c.clusters.filter(cl => cl.side == side) + if (side == 'left' && !c.clusters.find(cl => cl.side == 'left' && cl.name == 'fingers')) { + clusters.unshift(mirrorCluster(c.clusters.find(cl => cl.side == 'right' && cl.name == 'fingers')!)) + } + if (side == 'left' && !c.clusters.find(cl => cl.side == 'left' && cl.name == 'thumbs')) { + clusters.push(mirrorCluster(c.clusters.find(cl => cl.side == 'right' && cl.name == 'thumbs')!)) + } + } for (const clusterA of clusters) { for (const clusterB of clusterA.clusters) { for (const key of clusterB.keys) { @@ -523,14 +576,15 @@ export function toCuttleKey(c: CosmosKeyboard, cluster: CosmosCluster, col: Cosm } export function fromCosmosConfig(c: CosmosKeyboard, flipLeft = true): FullCuttleform { - return c.unibody - ? { - unibody: sideFromCosmosConfig(c, 'unibody', flipLeft), - } - : { - left: sideFromCosmosConfig(c, 'left', flipLeft), - right: sideFromCosmosConfig(c, 'right', flipLeft), - } + if (c.unibody) { + return { unibody: sideFromCosmosConfig(c, 'unibody', flipLeft) } + } + const result: FullCuttleform = { + left: sideFromCosmosConfig(c, 'left', flipLeft), + right: sideFromCosmosConfig(c, 'right', flipLeft), + } + if (hasCenter(c)) result.center = sideFromCosmosConfig(c, 'center', flipLeft) + return result } // /** Determines how much the thumb cluster shifts when the thumb origin is moved from */ @@ -540,7 +594,8 @@ export function fromCosmosConfig(c: CosmosKeyboard, flipLeft = true): FullCuttle /** Calculate the cluster trsf, accounting for the fact that everything rotates in flat mode by fingers key rotation */ export function calcClusterTrsf(cluster: CosmosCluster, keeb: CosmosKeyboard) { - if (cluster.name == 'fingers') return rotationPositionETrsf(cluster, false) + // Center clusters and fingers use absolute world-space positioning. + if (cluster.name == 'fingers' || cluster.side == 'center') return rotationPositionETrsf(cluster, false) const fingerCluster = keeb.clusters.find(c => c.side == cluster.side && c.name == 'fingers') || mirrorCluster(keeb.clusters.find(c => c.name == 'fingers')!) const fingerPos = rotationPositionETrsf(fingerCluster, false) || new ETrsf() @@ -687,7 +742,7 @@ function rotateColumnStagger(stagger: Trsf, col: number, curvature: Curvature) { return stagger.premultiplied(columnRotation.inverted()).multiply(columnRotation) } -function dominantClusterType(keys: CuttleKey[], side: 'right' | 'left') { +function dominantClusterType(keys: CuttleKey[], side: CosmosClusterSide) { const clusterNames = new TallyMap() const clusterTypes = new TallyMap() for (const key of keys) { @@ -695,7 +750,12 @@ function dominantClusterType(keys: CuttleKey[], side: 'right' | 'left') { if (key.position.history.find(h => h.name == 'placeOnMatrix')) clusterTypes.incr('matrix') if (key.position.history.find(h => h.name == 'placeOnSphere')) clusterTypes.incr('sphere') } - return encodeClusterFlags({ type: clusterTypes.max() ?? 'matrix', name: clusterNames.max() ?? 'fingers', side }) + // Side 'center' is encoded via name='center' (bit-side='right'). The runtime + // side is restored on decode; the bitfield only cares about the encoded name. + const isCenter = side === 'center' + const name = isCenter ? 'center' : (clusterNames.max() ?? 'fingers') + const sideForFlags: ClusterSide = isCenter ? 'right' : side + return encodeClusterFlags({ type: clusterTypes.max() ?? 'matrix', name, side: sideForFlags }) } function getTransformBy(key: CuttleKey) { @@ -719,7 +779,11 @@ function collectByTransformBy(keys: CuttleKey[]) { function collectByClusterName(keys: CuttleKey[]) { const names = new DefaultMap(() => []) for (const key of keys) { - const cluster = key.cluster == 'thumbs' ? key.cluster : 'fingers' + const cluster: ClusterName = key.cluster == 'thumbs' + ? 'thumbs' + : key.cluster == 'center' + ? 'center' + : 'fingers' names.get(cluster).push(key) } return names @@ -856,13 +920,21 @@ function nKeysInCluster(cl: CosmosCluster) { } /** Find the index of the nth key after filtering all keys to a certain side */ -export function nthIndex(conf: CosmosKeyboard, side: 'right' | 'left' | 'unibody', cuttleInd: number) { +export function nthIndex(conf: CosmosKeyboard, side: 'right' | 'left' | 'center' | 'unibody', cuttleInd: number) { if (!conf) return null const rightFingerCluster = conf.clusters.find(c => c.name == 'fingers' && c.side == 'right')! const rightThumbCluster = conf.clusters.find(c => c.name == 'thumbs' && c.side == 'right')! + const centerCluster = conf.clusters.find(c => c.side == 'center') const nRightKeys = nKeysInCluster(rightFingerCluster) + nKeysInCluster(rightThumbCluster) - const offset = side == 'right' ? 0 : nRightKeys + const nCenterKeys = centerCluster ? nKeysInCluster(centerCluster) : 0 + + // Center cluster is sorted between right and left (sortClusters places it at val=5..6). + if (side == 'center') { + return cuttleInd + nRightKeys + } + + const offset = side == 'right' ? 0 : nRightKeys + nCenterKeys const fingerCluster = conf.clusters.find(c => c.name == 'fingers' && c.side == side) const thumbCluster = conf.clusters.find(c => c.name == 'thumbs' && c.side == side) @@ -874,11 +946,18 @@ export function nthIndex(conf: CosmosKeyboard, side: 'right' | 'left' | 'unibody const leftThumbCluster = conf.clusters.find(c => c.name == 'thumbs' && c.side == 'left') const nFingerKeys = nKeysInCluster(leftFingerCluster || rightFingerCluster) - if (leftFingerCluster && leftThumbCluster) return cuttleInd - if (!leftFingerCluster && !leftThumbCluster) return cuttleInd - nRightKeys - if (!leftFingerCluster && leftThumbCluster) return cuttleInd - (cuttleInd < nRightKeys + nFingerKeys ? nRightKeys : nFingerKeys) - if (cuttleInd < nRightKeys + nFingerKeys) return cuttleInd - return cuttleInd - nFingerKeys + nKeysInCluster(rightFingerCluster) - nRightKeys + // Center keys sit between right and left in the global ordering. + if (centerCluster && cuttleInd >= nRightKeys && cuttleInd < nRightKeys + nCenterKeys) { + return cuttleInd + } + // Adjust cuttleInd back to the left-keys offset that the original logic expects. + const adjusted = cuttleInd - (centerCluster && cuttleInd >= nRightKeys ? nCenterKeys : 0) + + if (leftFingerCluster && leftThumbCluster) return adjusted + (centerCluster && cuttleInd >= nRightKeys ? nCenterKeys : 0) + if (!leftFingerCluster && !leftThumbCluster) return adjusted - nRightKeys + if (!leftFingerCluster && leftThumbCluster) return adjusted - (adjusted < nRightKeys + nFingerKeys ? nRightKeys : nFingerKeys) + if (adjusted < nRightKeys + nFingerKeys) return adjusted + return adjusted - nFingerKeys + nKeysInCluster(rightFingerCluster) - nRightKeys } if (cuttleInd < nFingerKeys) { @@ -907,6 +986,8 @@ function mirrorRotationTuple(t: bigint | undefined) { } export function mirrorCluster(clr: CosmosCluster, flipLetters = true): CosmosCluster { + // Center clusters live on the X-axis of symmetry; mirroring is a no-op. + if (clr.side === 'center') return clr return { ...clr, curvature: { ...clr.curvature }, diff --git a/src/lib/worker/config.serialize.ts b/src/lib/worker/config.serialize.ts index 5d383798..11867a3a 100644 --- a/src/lib/worker/config.serialize.ts +++ b/src/lib/worker/config.serialize.ts @@ -241,6 +241,7 @@ const KEYBOARD_EXTRA_DEFAULTS: KeyboardExtra = { wristRestRightExtension: 80, connectorLeftIndex: -10, connectorRightIndex: -10, + connectorCenterIndex: -10, screwIndices: [], microcontrollerAngle: 0, plateThickness: 30, @@ -413,11 +414,22 @@ export function deserializeCosmosConfig(b64: string): FullKeyboard { // return conf // } +/** Map decoded ClusterFlags to runtime cluster identity. When the bit-name is + * 'center', the runtime cluster has both side='center' and name='center' + * (the bit-side is meaningless for center clusters). */ +function flagsToClusterIdentity(idType: number) { + const flags = decodeClusterFlags(idType) + if (flags.name === 'center') { + return { side: 'center' as const, name: 'center' as const, type: flags.type } + } + return flags +} + export function decodeCosmosCluster(clusterA: Cluster): CosmosCluster { let lastCluster: Cluster | null = null return { - ...decodeClusterFlags(clusterA.idType ?? 0), + ...flagsToClusterIdentity(clusterA.idType ?? 0), curvature: decodeCurvature(clusterA.curvature || {}), profile: decodeProfile(clusterA.keyProfile || 0).profile, partType: decodePartType(clusterA.partType || 0), @@ -432,7 +444,7 @@ export function decodeCosmosCluster(clusterA: Cluster): CosmosCluster { let lastKey: Key | null = null let lastKeyRow = 0 return { - ...decodeClusterFlags(clusterB.idType ?? clusterA.idType ?? 0), + ...flagsToClusterIdentity(clusterB.idType ?? clusterA.idType ?? 0), curvature: decodeCurvature(clusterB.curvature || {}), profile: decodeProfile(clusterB.keyProfile || 0).profile, partType: decodePartType(clusterB.partType || 0), @@ -519,6 +531,7 @@ export function decodeConfigIdk(b64: string): CosmosKeyboard { wristRestPosition: keeb.wristRestPosition, connectorLeftIndex: keebExtra.connectorLeftIndex / 10, connectorRightIndex: keebExtra.connectorRightIndex / 10, + connectorCenterIndex: keebExtra.connectorCenterIndex / 10, clusters: keeb.cluster.map(decodeCosmosCluster), plate: hasSpecialPlate ? { @@ -609,9 +622,19 @@ function decodeCurvature(c: Curvature): Curvature { return curv } +/** Map a CosmosCluster (which may have side='center') to its bitfield representation. + * Center clusters encode bit-name='center', bit-side='right' (side ignored on decode). */ +function clusterFlagsForEncoding(c: CosmosCluster) { + if (c.name === 'center' || c.side === 'center') { + return { side: 'right' as const, name: 'center' as const, type: c.type } + } + // Non-center clusters always have side='left' or 'right' at this point. + return { side: c.side as 'left' | 'right', name: c.name as 'fingers' | 'thumbs', type: c.type } +} + export function encodeCosmosCluster(clusterA: CosmosCluster): Cluster { const cluster: Cluster = { - idType: encodeClusterFlags(clusterA), + idType: encodeClusterFlags(clusterFlagsForEncoding(clusterA)), cluster: [], key: [], partType: diff(encodePartType(clusterA.partType), 0), @@ -625,7 +648,7 @@ export function encodeCosmosCluster(clusterA: CosmosCluster): Cluster { for (const clusterB of clusterA.clusters) { const col = clusterB.column const column: Cluster = { - idType: diff(encodeClusterFlags(clusterB), cluster.idType), + idType: diff(encodeClusterFlags(clusterFlagsForEncoding(clusterB)), cluster.idType), cluster: [], key: [], partType: diff(encodePartType(clusterB.partType), 0), @@ -715,6 +738,7 @@ export function encodeCosmosConfig(conf: CosmosKeyboard): Keyboard { wristRestRightExtension: Math.round(conf.wristRestProps.extensionRight * 10), connectorLeftIndex: Math.round(conf.connectorLeftIndex * 10), connectorRightIndex: Math.round(conf.connectorRightIndex * 10), + connectorCenterIndex: Math.round(conf.connectorCenterIndex * 10), screwIndices: conf.screwIndices.some(c => c >= 0) ? conf.screwIndices.map(i => Math.round(i * 10) + 10) : [], roundedSideConcavity: conf.rounded.side ? Math.round(conf.rounded.side.concavity * 10) : undefined, roundedSideDivisor: conf.rounded.side ? Math.round(conf.rounded.side.divisor * 10) : undefined, diff --git a/src/lib/worker/config.test.ts b/src/lib/worker/config.test.ts index 5d3e1f34..f6b0963a 100644 --- a/src/lib/worker/config.test.ts +++ b/src/lib/worker/config.test.ts @@ -103,6 +103,65 @@ test('Ensure long letters are saved', () => { expect(decoded.clusters[0].clusters[0].keys[0].profile.letter).toEqual('LOOOONG') }) +test('Center cluster encoding round-trip', () => { + const config = cuttleConf(defaultConfig.options as any) + const cosmos = toCosmosConfig(config, 'right', true) + cosmos.wristRestPosition = 0n + + // Add a center cluster (replicates what setCenterCluster would produce) + cosmos.clusters.push({ + name: 'center', + side: 'center', + type: 'matrix', + curvature: {}, + profile: undefined, + partType: {}, + position: 1234567890n, + rotation: undefined, + clusters: [{ + name: 'center', + side: 'center', + type: 'matrix', + curvature: {}, + profile: undefined, + partType: {}, + column: 0, + clusters: [], + keys: [ + { partType: {}, profile: { row: 5, home: null }, row: 0, position: undefined, rotation: undefined }, + { partType: {}, profile: { row: 5, home: null }, row: 1, position: undefined, rotation: undefined }, + ], + position: undefined, + rotation: undefined, + }], + keys: [], + }) + cosmos.connectorCenterIndex = 7 + + const encoded = serializeCosmosConfig(encodeCosmosConfig(cosmos)) + const decoded = decodeConfigIdk(encoded) + + // Center cluster preserved with side='center', name='center' + const decodedCenter = decoded.clusters.find(c => c.side === 'center') + expect(decodedCenter).toBeDefined() + expect(decodedCenter!.name).toBe('center') + expect(decodedCenter!.side).toBe('center') + expect(decodedCenter!.position).toBe(1234567890n) + expect(decodedCenter!.clusters[0].keys.length).toBe(2) + expect(decodedCenter!.clusters[0].side).toBe('center') + expect(decodedCenter!.clusters[0].name).toBe('center') + expect(decoded.connectorCenterIndex).toBe(7) +}) + +test('Old URL without center cluster decodes cleanly', () => { + // The default-config URL — does NOT contain a center cluster + const encoded = + 'Cn8KDxIFEIA/ICcSABIAEgA4MQoPEgUQgEsgJxIAEgASADgdChwSBRCAVyAnEgASABIDELAvEgMQsF84CUCE8LwCChcSBRCAYyAnEgASABIDELA7EgMQsGs4CgoVEgUQgG8gJxIAEgASADgeQJCGirAHGABA6IWgrvBVSNzwoqABCpIBChcSExDAwAJAgICYAkjCmaCVkLwBUEM4CAoVEhAQQECAgCBI0JWA3ZD1A1ALUJ4CChYSEhBAQICAzAJIwpmglZC8AVCGAVA6ChQSEBBAQICA+AFI5pn8p5ALUFdQfwoVEhAQQECAgKQDSPCZzLXQMFB0UJUBGAIiCgjIARDIARgAIABAy4uEpNAxSK2R3I3BkwZyAA==' + const cosmos = decodeConfigIdk(encoded) + expect(cosmos.clusters.find(c => c.side === 'center')).toBeUndefined() + expect(cosmos.connectorCenterIndex).toBe(-1) +}) + test('Encoding connectors', () => { const connectors: ConnectorMaybeCustom[] = [ { preset: 'trrs' }, diff --git a/src/lib/worker/config.ts b/src/lib/worker/config.ts index 8ee69e30..1bb25963 100644 --- a/src/lib/worker/config.ts +++ b/src/lib/worker/config.ts @@ -143,6 +143,7 @@ export type Cuttleform = SpecificCuttleform export type FullCuttleform = { left?: Cuttleform right?: Cuttleform + center?: Cuttleform unibody?: Cuttleform } @@ -1524,6 +1525,7 @@ export function newFullGeometry(c: FullCuttleform): FullGeometry { const geo: FullGeometry = {} if (c.left) geo.left = newGeometry(c.left) if (c.right) geo.right = newGeometry(c.right) + if (c.center) geo.center = newGeometry(c.center) if (c.unibody) geo.unibody = newGeometry(c.unibody) return geo } @@ -1532,13 +1534,17 @@ export function setBottomZ(conf: FullCuttleform) { if (conf.left && conf.right) { const botLeft = newGeometry(conf.left).bottomZ const botRight = newGeometry(conf.right).bottomZ - conf.left.bottomZ = conf.right.bottomZ = Math.min(botLeft, botRight) + const botCenter = conf.center ? newGeometry(conf.center).bottomZ : Infinity + const bot = Math.min(botLeft, botRight, botCenter) + conf.left.bottomZ = conf.right.bottomZ = bot + if (conf.center) conf.center.bottomZ = bot } } export type Center = { left?: Point right?: Point + center?: Point unibody?: Point } export type FullCenter = Full
@@ -1555,23 +1561,40 @@ export function fullEstimatedCenter(geo: FullGeometry | undefined, withWristRest } else { const leftBB = estimatedBB(geo.left, withWristRest && !!geo.left?.c.wristRestRight) const rightBB = estimatedBB(geo.right, withWristRest && !!geo.right?.c.wristRestRight) - const sepDiff = (VIEW_SEPARATION - (rightBB[0] + leftBB[0])) / 2 + const centerBB = geo.center ? estimatedBB(geo.center) : null + // sepDiff enforces VIEW_SEPARATION between every adjacent pair. With a center cluster between + // left and right, two gaps exist (left↔center and center↔right), so the budget is + // 2*VIEW_SEPARATION plus the center's own width. + const centerWidth = centerBB ? centerBB[1] - centerBB[0] : 0 + const sepDiff = centerBB + ? (2 * VIEW_SEPARATION + centerWidth - (rightBB[0] + leftBB[0])) / 2 + : (VIEW_SEPARATION - (rightBB[0] + leftBB[0])) / 2 + const minY = Math.min(leftBB[2], rightBB[2], centerBB?.[2] ?? Infinity) + const maxY = Math.max(leftBB[3], rightBB[3], centerBB?.[3] ?? -Infinity) + const minZ = Math.min(leftBB[4], rightBB[4], centerBB?.[4] ?? Infinity) + const maxZ = Math.max(leftBB[5], rightBB[5], centerBB?.[5] ?? -Infinity) + const sharedY = (minY + maxY) / 2 + const sharedZ = (minZ + maxZ) / 2 + const both: Center = { + left: [(rightBB[1] - leftBB[1]) / 2 + sepDiff, sharedY, sharedZ], + right: [(rightBB[1] - leftBB[1]) / 2 - sepDiff, sharedY, sharedZ], + } + if (centerBB) { + // Center sits between left and right. Its X offset is the displayed midpoint of left-vs-right + // widths subtracted from the center's geometric midpoint. + const leftWidth = leftBB[1] - leftBB[0] + const rightWidth = rightBB[1] - rightBB[0] + both.center = [ + (centerBB[0] + centerBB[1]) / 2 - (leftWidth - rightWidth) / 2, + sharedY, + sharedZ, + ] + } return { left: { left: [-(leftBB[0] + leftBB[1]) / 2, (leftBB[2] + leftBB[3]) / 2, (leftBB[4] + leftBB[5]) / 2], }, - both: { - left: [ - (rightBB[1] - leftBB[1]) / 2 + sepDiff, - (Math.min(leftBB[2], rightBB[2]) + Math.max(leftBB[3], rightBB[3])) / 2, - (Math.min(leftBB[4], rightBB[4]) + Math.max(leftBB[5], rightBB[5])) / 2, - ], - right: [ - (rightBB[1] - leftBB[1]) / 2 - sepDiff, - (Math.min(leftBB[2], rightBB[2]) + Math.max(leftBB[3], rightBB[3])) / 2, - (Math.min(leftBB[4], rightBB[4]) + Math.max(leftBB[5], rightBB[5])) / 2, - ], - }, + both, right: { right: [(rightBB[0] + rightBB[1]) / 2, (rightBB[2] + rightBB[3]) / 2, (rightBB[4] + rightBB[5]) / 2], }, @@ -1589,10 +1612,16 @@ export function fullEstimatedSize(geo: FullGeometry | undefined): Full<[number, } else { const [lx1, lx2, ly1, ly2, lz1, lz2] = estimatedBB(geo.left) const [rx1, rx2, ry1, ry2, rz1, rz2] = estimatedBB(geo.right) + const cBB = geo.center ? estimatedBB(geo.center) : null const sep = VIEW_SEPARATION - (rx1 + lx1) + const cWidth = cBB ? cBB[1] - cBB[0] : 0 return { left: [lx2 - lx1, ly2 - ly1, lz2 - lz1], - both: [sep + rx2 + lx2, Math.max(ly2, ry2) - Math.min(ly1, ry1), Math.max(lz2, rz2) - Math.min(lz1, rz1)], + both: [ + sep + rx2 + lx2 + cWidth, + Math.max(ly2, ry2, cBB?.[3] ?? -Infinity) - Math.min(ly1, ry1, cBB?.[2] ?? Infinity), + Math.max(lz2, rz2, cBB?.[5] ?? -Infinity) - Math.min(lz1, rz1, cBB?.[4] ?? Infinity), + ], right: [rx2 - rx1, ry2 - ry1, rz2 - rz1], } } diff --git a/src/proto/cosmos.proto b/src/proto/cosmos.proto index 621374ec..6e41d99e 100644 --- a/src/proto/cosmos.proto +++ b/src/proto/cosmos.proto @@ -73,6 +73,7 @@ message KeyboardExtra { repeated uint32 foot_indices = 20; optional uint32 foot_diameter = 21; optional uint32 plate_art = 22; + optional int32 connector_center_index = 23; } message Curvature { diff --git a/src/proto/cosmosStructs.ts b/src/proto/cosmosStructs.ts index ff71e81f..da89e2dd 100644 --- a/src/proto/cosmosStructs.ts +++ b/src/proto/cosmosStructs.ts @@ -74,7 +74,7 @@ enumeration('SCREW_TYPE', ['screw insert', 'tapered screw insert', 'expanding sc enumeration('SCREW_SIZE', ['M3', 'M4', '#4-40', '#6-32']) enumeration('CLUSTER_TYPE', ['matrix', 'sphere']) -enumeration('CLUSTER_NAME', ['fingers', 'thumbs']) +enumeration('CLUSTER_NAME', ['fingers', 'thumbs', 'center']) enumeration('CLUSTER_SIDE', ['right', 'left']) enumeration('MICROCONTROLLER_NAME', [ diff --git a/src/routes/beta/App.svelte b/src/routes/beta/App.svelte index 51ea0b26..6fb9c7a3 100644 --- a/src/routes/beta/App.svelte +++ b/src/routes/beta/App.svelte @@ -250,6 +250,13 @@ plate: c.right.plate ? { ...c.right.plate } : undefined, } : undefined, + center: c.center + ? { + ...c.center, + shell: { ...c.center.shell }, + plate: c.center.plate ? { ...c.center.plate } : undefined, + } + : undefined, unibody: c.unibody ? { ...c.unibody, @@ -278,12 +285,13 @@ ...new Set([ ...areDifferent(c1.left, c2.left), ...areDifferent(c1.right, c2.right), + ...areDifferent(c1.center, c2.center), ...areDifferent(c1.unibody, c2.unibody), ]), ] } - const calcOtherPromises = (conf: Cuttleform, side: 'left' | 'right' | 'unibody') => ({ + const calcOtherPromises = (conf: Cuttleform, side: 'left' | 'right' | 'center' | 'unibody') => ({ intersectionsPromise: pool.execute( (w) => w.intersections(conf, side) as Promise, 'Intersections' @@ -449,7 +457,9 @@ else { delete meshes.left delete meshes.right + delete meshes.center } + if (!kbdNames.includes('center')) delete meshes.center } if (!flags.fast && full) { @@ -712,7 +722,7 @@ class:selected={$view == 'left'}> { const trsfs: Trsf[] = [] - const sides: ('left' | 'right')[] = [] + const sides: ('left' | 'right' | 'center')[] = [] const cpts = mapKeys(p, (k, col, c) => { const trsf = cosmosKeyPosition(k, col, c, p).evaluate({ flat: true }) trsfs.push(trsf) diff --git a/src/routes/beta/lib/dialogs/AssemblyView.svelte b/src/routes/beta/lib/dialogs/AssemblyView.svelte index cccd5083..09e8d7e9 100644 --- a/src/routes/beta/lib/dialogs/AssemblyView.svelte +++ b/src/routes/beta/lib/dialogs/AssemblyView.svelte @@ -12,7 +12,7 @@ import Dialog from '$lib/presentation/Dialog.svelte' export let center: Center - export let meshes: ['left' | 'right' | 'unibody', KeyboardMeshes][] + export let meshes: ['left' | 'right' | 'center' | 'unibody', KeyboardMeshes][] export let geometry: FullGeometry export let size: [number, number, number] diff --git a/src/routes/beta/lib/dialogs/DownloadDialog.svelte b/src/routes/beta/lib/dialogs/DownloadDialog.svelte index 319a400b..e67d4865 100644 --- a/src/routes/beta/lib/dialogs/DownloadDialog.svelte +++ b/src/routes/beta/lib/dialogs/DownloadDialog.svelte @@ -44,7 +44,7 @@ const defaultEmail = lastUser.success && lastUser.method == 'Email' ? lastUser.user.login : '' console.log('defaultEmail', defaultEmail, lastUser) - function downloadSTEP(side: 'left' | 'right' | 'unibody') { + function downloadSTEP(side: 'left' | 'right' | 'center' | 'unibody') { numDownloaded += 1 if (!config) { generatingError = new Error('Configuration has not yet been evaluated') @@ -78,7 +78,7 @@ ) } - function downloadSTL(model: string, side: 'left' | 'right' | 'unibody' | 'test') { + function downloadSTL(model: string, side: 'left' | 'right' | 'center' | 'unibody' | 'test') { numDownloaded += 1 if (!config) { generatingError = new Error('Configuration has not yet been evaluated') @@ -140,7 +140,7 @@ return new Blob([contents], { type: blob.type }) } - async function downloadGLB(side: 'left' | 'right' | 'unibody') { + async function downloadGLB(side: 'left' | 'right' | 'center' | 'unibody') { numDownloaded += 1 generatingGLB = true const scene = await modelAsScene(pool, newFullGeometry(config), side) @@ -151,20 +151,22 @@ generatingGLB = false } - function iconPath(kbdName: 'left' | 'right' | 'unibody') { + function iconPath(kbdName: 'left' | 'right' | 'center' | 'unibody') { if (kbdName == 'left') return mdiHandBackLeft if (kbdName == 'right') return mdiHandBackRight return mdiKeyboard } - function kbdName(kbdName: 'left' | 'right' | 'unibody', name = '') { + function kbdName(kbdName: 'left' | 'right' | 'center' | 'unibody', name = '') { if (name) { if (kbdName == 'left') return 'L / ' + name if (kbdName == 'right') return 'R / ' + name + if (kbdName == 'center') return 'C / ' + name return name } if (kbdName == 'left') return 'Left' if (kbdName == 'right') return 'Right' + if (kbdName == 'center') return 'Center' return 'Unibody' } diff --git a/src/routes/beta/lib/editor/PeaConfig.svelte b/src/routes/beta/lib/editor/PeaConfig.svelte index e5661cc1..e2f4df7a 100644 --- a/src/routes/beta/lib/editor/PeaConfig.svelte +++ b/src/routes/beta/lib/editor/PeaConfig.svelte @@ -43,11 +43,21 @@ $: anyConfig = config.right || config.unibody || { microcontroller: undefined } $: truncated = anyConfig.microcontroller == 'lemon-wireless' && $modelName.length > 16 + $: hasCenterPiece = !!config.center + $: isLemon = anyConfig.microcontroller?.startsWith('lemon') + $: warnCenterFirmware = hasCenterPiece && isLemon

Successfully made the matrix!

Now you can download code for your microcontroller.

+{#if warnCenterFirmware} + + Firmware will not be generated for the center cluster. The center cluster's keys must be wired + manually. + +{/if} +
Keyboard / File Name:
diff --git a/src/routes/beta/lib/editor/VisualEditor2.svelte b/src/routes/beta/lib/editor/VisualEditor2.svelte index 30a6735d..dea9634d 100644 --- a/src/routes/beta/lib/editor/VisualEditor2.svelte +++ b/src/routes/beta/lib/editor/VisualEditor2.svelte @@ -39,6 +39,7 @@ } from '$lib/geometry/microcontrollers' import { fromCosmosConfig, + hasCenter, mirrorCluster, partVariant, toCosmosConfig, @@ -57,18 +58,26 @@ import DecimalInputInherit from './DecimalInputInherit.svelte' import { clusterAngle, + clusterLeftSeparation, + clusterRightSeparation, clusterSeparation, connectorsString, + getCenterClusterN, getNKeys, getSize, getThumbN, hasInnerCol, hasKey, hasOuterCol, + isCenterCluster, isFnKey, isNumKey, isThumb, + removeCenterCluster, + setCenterCluster, setClusterAngle, + setClusterLeftSeparation, + setClusterRightSeparation, setClusterSeparation, setClusterSize, setThumbCluster, @@ -441,10 +450,13 @@ $: leftThumbCluster = $protoConfig.clusters.find((c) => c.name == 'thumbs' && c.side == 'left') $: rightFingersCl = $protoConfig.clusters.find((c) => c.name == 'fingers' && c.side == 'right')! $: leftFingersCl = $protoConfig.clusters.find((c) => c.name == 'fingers' && c.side == 'left') + $: centerCl = $protoConfig.clusters.find((c) => c.side == 'center') $: tempFingersCluster = $tempConfig.clusters.find((c) => c.name == 'fingers')! $: whichRight = getThumbN($protoConfig, 'right') $: whichLeft = getThumbN($protoConfig, 'left') + $: whichCenter = getCenterClusterN($protoConfig) + $: hasCenterCl = hasCenter($protoConfig) const rotationStore = new TupleStore(-1n, 45, true) const [rotationX, rotationY, rotationZ, _] = rotationStore.components() @@ -464,6 +476,18 @@ const setClusterSep = (ev: CustomEvent) => protoConfig.update((proto) => setClusterSeparation(proto, ev.detail)) + $: clusterLeftSep = clusterLeftSeparation($tempConfig) + $: clusterRightSep = clusterRightSeparation($tempConfig) + const setClusterLeftSep = (ev: CustomEvent) => + protoConfig.update((proto) => setClusterLeftSeparation(proto, ev.detail)) + const setClusterRightSep = (ev: CustomEvent) => + protoConfig.update((proto) => setClusterRightSeparation(proto, ev.detail)) + + function changeCenterCluster(which: 'manuform', e: Event) { + const n = Number((e.target as HTMLInputElement).value) + protoConfig.update((p) => setCenterCluster(p, which, n)) + } + $: clusterAng = clusterAngle($tempConfig) const setClusterAng = (ev: CustomEvent) => protoConfig.update((proto) => setClusterAngle(proto, ev.detail)) @@ -832,6 +856,50 @@ {/if} +
+ protoConfig.update(removeCenterCluster)} + selected={!centerCl || centerCl.clusters.length == 0} + > + protoConfig.update((p) => setCenterCluster(p, 'manuform'))} + selected={isCenterCluster($protoConfig, 'manuform')} + /> + {#if whichCenter} + {@const which = whichCenter.which} + + + + {/if} + {#if centerCl && centerCl.clusters.length > 0} + + + + + + + {#if !basic} + + + + + + + {/if} + {/if} +
-
+
{#each configKeys as kbd}