Skip to content

feat(core): add pattern algebra transforms on HapticPattern #88

Description

@l2hyunwoo

Summary

Compiled patterns should be manipulable as plain values. This adds pure transforms on HapticPattern, each returning a new pattern:

fun HapticPattern.scaleIntensity(factor: Float): HapticPattern
fun HapticPattern.timeStretch(factor: Float): HapticPattern
fun HapticPattern.reversed(): HapticPattern
infix fun HapticPattern.then(other: HapticPattern): HapticPattern
operator fun HapticPattern.plus(other: HapticPattern): HapticPattern
fun HapticPattern.repeated(times: Int): HapticPattern

HapticPattern.events is already public, so these are extension functions over the event list. No executor or platform code is involved.

Design decisions

  • scaleIntensity maps every intensity to HapticIntensity.Custom(value * factor). The Custom constructor already clamps to [0, 1] (HapticIntensity.kt:61), so no extra guard is needed. Predefined levels are downgraded to Custom, since a scaled STRONG has no predefined equivalent. Negative factors are rejected with require.
  • timeStretch scales event edges, not durations: newStart = round(start * f), newEnd = round((start + duration) * f), newDuration = newEnd - newStart. Rounding start and duration independently would let adjacent event boundaries drift apart.
  • reversed() mirrors each event on the pattern span: newStart = span - (start + duration). Transforms may produce overlapping events and do not resolve them; playback serializes overlaps through mergeToSerial (highest intensity wins). That is why this work depends on feat(core): serial-timeline waveform pipeline (overlap, fall-ramp, handle lifecycle) #85.
  • repeated is defined on top of then, so repeated(n) equals folding then n times by construction.
  • rawSpanMs() (executor/RawSpanMs.kt:27) and PatternElement.totalDurationMs (element/PatternElement.kt:55) compute the same max end time over events. Both delegate to a single internal HapticPattern.spanMs() after this change. The tree-sum totalDurationMs overrides on SequenceElement/RepeatElement are sequential-scheduling math, not span, and stay untouched.

Acceptance criteria

Property tests (kotest-property, same idiom as InsertFallRampsTest):

  • p.reversed().reversed() == p
  • p.timeStretch(1f) == p; p.timeStretch(a).timeStretch(b) within events.size ms of p.timeStretch(a * b)
  • span(p.timeStretch(f)) equals round(span(p) * f) within rounding tolerance
  • intensity stays in [0, 1] after any scaleIntensity
  • then is associative; the empty pattern is its identity; span(a then b) == span(a) + span(b)
  • p.repeated(1) == p; p.repeated(0) is empty; event count multiplies by times

One top-level declaration per file, apiDump regenerated.

Depends on #85.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions