diff --git a/src/biaxial-stepper-element.stories.ts b/src/biaxial-stepper-element.stories.ts new file mode 100644 index 0000000..75eb84d --- /dev/null +++ b/src/biaxial-stepper-element.stories.ts @@ -0,0 +1,143 @@ +import { html } from 'lit'; +import './biaxial-stepper-element'; + +export default { + title: 'Biaxial Stepper', + component: 'wokwi-biaxial-stepper', + argTypes: { + innerHandLength: { control: { type: 'range', min: 20, max: 70 } }, + innerHandAngle: { control: { type: 'range', min: 0, max: 360 } }, + innerHandColor: { control: { type: 'color' } }, + innerHandShape: { options: ['arrow', 'plain', 'ornate'], control: { type: 'select' } }, + outerHandLength: { control: { type: 'range', min: 20, max: 70 } }, + outerHandAngle: { control: { type: 'range', min: 0, max: 360 } }, + outerHandColor: { control: { type: 'color' } }, + outerHandShape: { options: ['arrow', 'plain', 'ornate'], control: { type: 'select' } }, + }, + args: { + outerHandLength: 20, + outerHandAngle: 0, + outerHandColor: 'grey', + outerHandShape: 'plain', + innerHandLength: 20, + innerHandAngle: 0, + innerHandColor: 'darkgrey', + innerHandShape: 'plain', + }, +}; + +const Template = ({ + innerHandLength, + innerHandColor, + innerHandShape, + innerHandAngle, + outerHandLength, + outerHandColor, + outerHandShape, + outerHandAngle, +}) => html``; + +export const Default = Template.bind({}); +Default.args = { + innerHandLength: 70, + innerHandColor: 'silver', + innerHandShape: 'plain', + innerHandAngle: 90, + outerHandLength: 70, + outerHandColor: 'gold', + outerHandShape: 'plain', + outerHandAngle: 270, +}; + +export const NineOclock = Template.bind({}); +NineOclock.args = { + innerHandLength: 70, + innerHandColor: 'silver', + innerHandShape: 'plain', + innerHandAngle: 0, + outerHandLength: 40, + outerHandColor: 'gold', + outerHandShape: 'plain', + outerHandAngle: 270, +}; + +export const SixOclock = Template.bind({}); +SixOclock.args = { + innerHandLength: 70, + innerHandColor: 'silver', + innerHandShape: 'plain', + innerHandAngle: 0, + outerHandLength: 40, + outerHandColor: 'gold', + outerHandShape: 'plain', + outerHandAngle: 180, +}; + +export const ThreeOclock = Template.bind({}); +ThreeOclock.args = { + innerHandLength: 70, + innerHandColor: 'silver', + innerHandShape: 'plain', + innerHandAngle: 0, + outerHandLength: 50, + outerHandColor: 'gold', + outerHandShape: 'plain', + outerHandAngle: 90, +}; + +export const TenPastTen = Template.bind({}); +TenPastTen.args = { + innerHandLength: 70, + innerHandColor: 'silver', + innerHandShape: 'plain', + innerHandAngle: 60, + outerHandLength: 60, + outerHandColor: 'gold', + outerHandShape: 'plain', + outerHandAngle: 300, +}; + +export const SameLength = Template.bind({}); +SameLength.args = { + innerHandLength: 30, + innerHandColor: 'blue', + innerHandShape: 'plain', + innerHandAngle: 0, + outerHandLength: 30, + outerHandColor: 'green', + outerHandShape: 'plain', + outerHandAngle: 180, +}; + +export const LongArrows = Template.bind({}); +LongArrows.args = { + innerHandLength: 70, + innerHandColor: 'blue', + innerHandShape: 'arrow', + innerHandAngle: 90, + outerHandLength: 70, + outerHandColor: 'green', + outerHandShape: 'arrow', + outerHandAngle: 270, +}; + +export const OrnateClock = Template.bind({}); +OrnateClock.args = { + innerHandLength: 70, + innerHandColor: 'silver', + innerHandShape: 'ornate', + innerHandAngle: 60, + outerHandLength: 60, + outerHandColor: 'gold', + outerHandShape: 'ornate', + outerHandAngle: 300, +}; diff --git a/src/biaxial-stepper-element.ts b/src/biaxial-stepper-element.ts new file mode 100644 index 0000000..50cdc44 --- /dev/null +++ b/src/biaxial-stepper-element.ts @@ -0,0 +1,212 @@ +import { html, LitElement, svg } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { ElementPin } from '.'; +import { mmToPix } from './utils/units'; + +type HandShape = 'arrow' | 'plain' | 'ornate'; +type HandDesc = { xOff: number; yOff: number; path: string }; + +const SHAFT_X = 60; // x location of shaft point +const SHAFT_Y = 77; // y location of shaft point +const OUTER_OFFSET = 9; // offset to center of outer hand's ring +const INNER_OFFSET = 4.7; // offset to center of inner hand's ring +const ORNATE_OUTER_OFFSET = 9; // offset to center of outer ornate hand's ring +const ORNATE_INNER_OFFSET = 5; // offset to center of inner ornate hand's ring +const BASE_MOVE_THOLD = 30; // threshold above which we need to move the base to allow for hands extending outside the base + +@customElement('wokwi-biaxial-stepper') +export class BiaxialStepperElement extends LitElement { + // length used to control outer hand length in mm + @property() outerHandLength = 30; + + // the current hand angle in degrees + @property() outerHandAngle = 0; + + // the current outer hand color + @property() outerHandColor = 'gold'; + + // the current outer hand shape + @property() outerHandShape: 'arrow' | 'plain' | 'ornate' = 'plain'; + + // length used to control inner hand length in mm + @property() innerHandLength = 30; + + // the current hand angle in degrees + @property() innerHandAngle = 0; + + // the current inner hand color + @property() innerHandColor = 'silver'; + + // the current inner hand shape + @property() innerHandShape: 'arrow' | 'plain' | 'ornate' = 'plain'; + + get pinInfo(): ElementPin[] { + const pinXY = (y: number) => { + return { x: 45, y: (28.9 + y * 2.54) * mmToPix }; + }; + + return [ + { name: 'A1+', ...pinXY(0), number: 1, signals: [] }, + { name: 'A1-', ...pinXY(1), number: 2, signals: [] }, + { name: 'B1+', ...pinXY(2), number: 3, signals: [] }, + { name: 'B1-', ...pinXY(3), number: 4, signals: [] }, + { name: 'A2+', ...pinXY(4), number: 5, signals: [] }, + { name: 'A2-', ...pinXY(5), number: 6, signals: [] }, + { name: 'B2+', ...pinXY(6), number: 7, signals: [] }, + { name: 'B2-', ...pinXY(7), number: 8, signals: [] }, + ]; + } + + readonly handMap: { [key: string]: (len: number) => HandDesc } = { + outerPlainHand: (len: number) => ({ + xOff: OUTER_OFFSET, + yOff: OUTER_OFFSET, + path: `m0,0 c0,5,4,9,9,9,3.28,0,6.13-1.73,7.7-4.33v.03c.5-.8,1.2-1.6,2.1-2.1,.8-.5,1.8-.8,2.8-.8 h 20 h${len}c1,0,1.8-.8,1.8-1.8s-.8-1.8-1.8-1.8h-${len} h -20 c-1,0-1.9-.3-2.8-.8-.8-.5-1.6-1.2-2.1-2h0c-1.56-2.64-4.43-4.4-7.74-4.4-5,0-9,4.1-9,9Zm3.5,0c0-3,2.4-5.4,5.5-5.4s5.5,2.4,5.5,5.4-2.5,5.4-5.5,5.4-5.5-2.4-5.5-5.4Z`, + }), + + outerArrowHand: (len: number): HandDesc => ({ + xOff: OUTER_OFFSET, + yOff: OUTER_OFFSET, + path: `m 0 0 c 0 5 4 9 9 9 c 3.89 0 7.16 -2.42 8.43 -5.85 c 0.3 -0.58 0.78 -1.16 1.27 -1.45 c 0.6 -0.3 1.29 -0.4 1.99 -0.2 c 0 0 0 0 0 0 h 0.01 s 0.9 0.2 0.9 0.2 h 20 l ${len} -1.7 l -${len} -1.9 h -20 l -0.9 0.3 c -0.7 0.2 -1.4 0.1 -2 -0.2 c -0.6 -0.3 -1.1 -0.8 -1.3 -1.5 c 0 0 -0.01 0.02 -0.02 0.02 c -1.38 -3.72 -4.38 -5.72 -8.38 -5.72 c -5 0 -9 4 -9 9 Z m 3.5 0 c 0 -3.1 2.5 -5.5 5.5 -5.5 s 5.5 2.5 5.5 5.5 s -2.5 5.5 -5.5 5.5 s -5.5 -2.5 -5.5 -5.5 Z`, + }), + + outerOrnateHand: (len: number): HandDesc => ({ + xOff: ORNATE_OUTER_OFFSET, + yOff: ORNATE_OUTER_OFFSET, + path: `m 0 0 c 0 5 4 9 9 9 c 4.951 0.028 9.683 -3.101 9.6 -7.6 c 0 0 1.1 1.4 3.3 1.2 c -0.2 1 0.3 1.2 0.3 1.2 c 0.6 -1.5 6.4 -6.3 8.6 -0.3 c -3.1 -0.3 -1.8 3.8 0.5 1.6 c 0 0 0.9 2.3 4 2.1 c -0.8 1.7 3.1 2.1 1.7 -0.4 c 0 0 2.7 -1 2.3 -3.9 c 0 0 0.8 0.6 2.6 0.6 c 2.7 0.2 6.2 -3.7 16 -2.9 l 0.6 -0.7 l -0.6 -0.8 c -8.9 0.7 -13 -2.3 -16 -3 c -1.9 0 -2.5 0.8 -2.5 0.8 c 0 0 0.3 -2.6 -2.5 -4.1 c 0.8 -0.8 0 -1.6 -0.8 -1.6 c -0.5 0 -1 0.4 -0.8 1.5 c 0 0 -2.8 -0.8 -4 2.1 c 0 0 -1.4 -2 -2.5 0.3 c 0.2 0.5 0 0.9 0.8 1.2 c 0.2 0.3 1.2 -0.2 1.2 -0.2 c 0 0 -0.6 3.2 -3.9 3 c 0 0 -2.7 0.1 -4.8 -3.3 c -0.3 0.5 -0.3 0.7 -0.3 0.7 v 0.7 c 0 0 -2.1 -0.4 -3.4 1.2 c -0.06 -4.37 -4.449 -7.347 -9.41 -7.385 c -5 0 -9 4 -9 9 z m 44 -2.2 c 2.1 1.2 4.1 1.5 4.1 1.5 c -0.4 0.7 -0.5 0.7 0 1.6 c 0 0 -2.6 0.2 -4.1 1.4 c 0 0 0.2 -1.7 -0.6 -2.2 c 1 -0.6 0.6 -2.2 0.6 -2.2 z m -2.3 -0.5 c 1.3 0.1 1.6 1.1 1.6 1.1 c 0.1 0.4 -1.2 0.8 -1.1 1.1 l 0.3 0.5 l -0.2 0.6 c 0 0.3 0.6 0.4 1.2 1 c -0.5 0.6 -0.5 1.3 -2.1 1.3 c -1.1 0 -3.1 -2.5 -4.2 -2.9 c 1.1 -0.1 2.5 -2.8 4.5 -2.8 z m -10.8 0.9 c 0.5 1 2 1.9 2 1.9 c -1.4 0.7 -2 1.7 -2 1.7 c -1 -1.2 -2.2 -1.9 -2.2 -1.9 c 1.3 -0.6 2.2 -1.8 2.2 -1.8 z m -10.1 0.4 c 1.4 0 1.5 1.1 3.656 1.552 c -2 0.3 -2.4 1.5 -3.6 1.4 c -1.5 -0.1 -1.6 -1.3 -1.6 -1.3 c 0 0 -0.2 -1.5 1.6 -1.6 c 0 0 0 0 0.1 0 z m 14.1 2.1 c 3.1 0.1 3.1 3.2 3.1 3.2 c -0.3 1.7 -2.5 2.6 -2.5 2.6 c -2.5 0.2 -3.3 -1.5 -3.3 -1.5 c 0 0 1.4 0.6 1.8 -1 c -0.4 -2 -2.2 -0.8 -2.2 -0.8 c 0.3 -2.3 3 -2.5 3 -2.5 z`, + }), + + innerPlainHand: (len: number): HandDesc => ({ + xOff: INNER_OFFSET, + yOff: INNER_OFFSET, + path: `m0,0 c 0 2.6 2.1 4.7 4.7 4.7 c 1.27 0 2.41 -0.5 3.25 -1.31 h 0 c 0.5 -0.49 1.1 -0.89 1.8 -1.19 c 0.7 -0.3 1.4 -0.4 2.1 -0.4 h 25 h ${len} c 1 0 1.8 -0.8 1.8 -1.8 s -0.8 -1.8 -1.8 -1.8 h -${len} h -25 s -0.06 0 -0.06 0 c -0.7 0 -1.5 -0.1 -2.1 -0.4 c -0.7 -0.3 -1.3 -0.7 -1.8 -1.2 h 0 c -0.84 -0.8 -1.98 -1.3 -3.24 -1.3 c -2.6 0 -4.7 2.1 -4.7 4.7 Z m 2.7 0 c 0 -1.1 0.9 -2 2 -2 s 2 0.9 2 2 s -0.9 2 -2 2 s -2 -0.9 -2 -2 Z`, + }), + + innerArrowHand: (len: number): HandDesc => ({ + xOff: INNER_OFFSET, + yOff: INNER_OFFSET, + path: `m 0 0 c 0 2.6 2.1 4.7 4.7 4.7 c 1.37 0 2.59 -0.59 3.44 -1.52 c 0.7 -0.7 1.5 -1.2 2.5 -1.5 s 2 -0.3 2.9 -0.1 l 0.9 0.2 l ${len} -1.8 l -${len} -1.8 l -0.9 0.2 c -1 0.2 -2 0.2 -2.9 -0.1 c -1 -0.3 -1.8 -0.8 -2.5 -1.5 c -0.86 -0.93 -2.08 -1.52 -3.44 -1.52 c -2.6 0 -4.7 2.1 -4.7 4.7 z m 2.7 0 c 0 -1.1 0.9 -2 2 -2 s 2 0.9 2 2 s -0.9 2 -2 2 s -2 -0.9 -2 -2 z`, + }), + + innerOrnateHand: (len: number): HandDesc => ({ + xOff: ORNATE_INNER_OFFSET, + yOff: ORNATE_INNER_OFFSET, + path: `m 0 0 c 0 5 5 5 5 5 c 5 0 4.6565 -5.696 5 -2.6 l 0.028 1.101 l 1.4525 0.016 l 0.55 -1.5 c 0 0 1.05 1.55 3.3 1.75 c 2.35 0.05 3.2 -0.85 3.2 -0.85 l 0.05 0.75 l 0.95 -0.05 l -0.05 1 l 4 -2.4 c 0.4 1.35 1.75 1.5 1.75 1.5 c 0.65 0.1 1.85 -0.95 1.85 -0.95 l 0.7 1.45 l 1 -1.75 l 1 1 l 0.35 -1.45 c 2.4 1.45 4.15 1.2 4.15 1.2 c 2.95 -0.1 3.9 -1.05 6.6 -1.95 c 4.85 -0.7 10.3 -0.1 10.3 -0.1 l -0.25 1.65 l 3.85 -1.45 l 15.2 -0.3 l 0.45 -0.4 l -0.55 -0.4 l -15.4 -0.25 l -3.6 -1.55 l 0.4 1.8 c 0 0 -4.75 -0.85 -10.1 -0.25 l -0.25 -1.05 l -1.05 0.85 c 0 0 0.45 -2.2 0.25 -3.8 c -1.15 2.55 -1.95 6.25 -6.45 6.3 c -4.4 -0.35 -5.7 -4.05 -5.7 -4.05 l -0.45 1 l -0.5 -0.5 l -0.55 0.75 c 0 0 -0.85 -0.6 -2.35 -0.75 c -1.85 0.15 -2.35 0.45 -2.35 0.45 l -0.25 -1.45 l -1.2 1.25 l -0.5 -0.8 l -1.05 1.05 l -0.6 -0.85 c -0.6 -0.45 -1.15 -0.55 -1.75 -0.3 c 0 0 -0.6 0.25 -0.8 0.75 c 0 0 -0.45 -0.7 -1.2 -0.85 c -0.75 -0.05 -2.3 1.3 -2.3 1.3 l -0.6175 -2.289 l -1.469 -0.032 l -0.0165 1.2265 c -0.3785 2.4515 0.0485 -3.212 -5.036 -3.228 c -4 0 -5.004 2.518 -5 5 z m 13.25 -1.2 c 0 0 0.15 1.3 1.05 1.3 c 0 0 0.5 -0.25 0.7 -0.8 c 0 0 0.35 0.85 0.75 0.9 c 0.9 -0.1 0.85 -1.25 1.15 -1.3 c 0 0 0.9 0.35 0.9 2.15 c 0 0 -0.6 1.7 -3 1.65 c 0 0 -2.05 -0.15 -2.55 -1.95 c 0 0 -0.15 -1.5 0.95 -2 z m 10.5 0.55 c 1.05 0 1.4 0.25 1.4 0.25 c 0.75 0.45 1 1.2 1 1.2 c 0.05 1.6 -0.85 1.55 -1.65 1.6 c 0 0 -0.95 -0.35 -1.3 -1.35 l -3.1 1.75 c 0 0 0.45 -2.5 2.8 -3.45 c 0.35 -0.05 0.6 -0.05 0.85 -0.05 z`, + }), + }; + + private cap(s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); + } + + private constrain(v: number, min: number, max: number): number { + return Math.max(min, Math.min(v, max)); + } + + private handPath(pfx: string, shape: string, len: number) { + let path = this.handMap[pfx + this.cap(shape) + 'Hand']; + + // apply default values to properties if invalid + if (path === undefined) { + path = this.handMap[pfx + 'PlainHand']; + } + return path(len); + } + render() { + const innerHandLength = this.constrain(this.innerHandLength, 20, 70); + const outerHandLength = this.constrain(this.outerHandLength, 20, 70); + + const innerPathDesc = this.handPath('inner', this.innerHandShape, innerHandLength); + const outerPathDesc = this.handPath('outer', this.outerHandShape, outerHandLength); + + const x = SHAFT_X; + const y = SHAFT_Y; + + return html` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `; + } +} diff --git a/src/index.ts b/src/index.ts index 6c40d20..386fcbe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,3 +50,4 @@ export { DipSwitch8Element } from './dip-switch-8-element'; export { StepperMotorElement } from './stepper-motor-element'; export { HX711Element } from './hx711-element'; export { KS2EMDC5Element } from './ks2e-m-dc5-element'; +export { BiaxialStepperElement } from './biaxial-stepper-element'; diff --git a/src/react-types.ts b/src/react-types.ts index 9be81b7..abe8449 100644 --- a/src/react-types.ts +++ b/src/react-types.ts @@ -49,6 +49,7 @@ import { DipSwitch8Element } from './dip-switch-8-element'; import { StepperMotorElement } from './stepper-motor-element'; import { HX711Element } from './hx711-element'; import { KS2EMDC5Element } from './ks2e-m-dc5-element'; +import { BiaxialStepperElement } from './biaxial-stepper-element'; type WokwiElement = Partial & React.ClassAttributes; @@ -103,6 +104,7 @@ declare global { 'wokwi-stepper-motor': WokwiElement; 'wokwi-hx711': WokwiElement; 'wokwi-ks2e-m-dc5': WokwiElement; + 'wokwi-biaxial-stepper': WokwiElement; } } }