Skip to content

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

Merged
l2hyunwoo merged 8 commits into
mainfrom
feat/pattern-algebra
Jul 21, 2026
Merged

feat(core): add pattern algebra transforms on HapticPattern#96
l2hyunwoo merged 8 commits into
mainfrom
feat/pattern-algebra

Conversation

@l2hyunwoo

@l2hyunwoo l2hyunwoo commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Closes #88.

Adds pure value transforms on HapticPattern. Each returns a new pattern; no executor or platform code is involved.

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

Decisions

  • scaleIntensity maps every intensity to HapticIntensity.Custom(value * factor); the Custom constructor already clamps to [0, 1], so factor > 1 saturates safely. Rejects negative factors.
  • timeStretch rounds the two event edges (start and end), not start and duration independently, so touching events stay touching. iosParameters (the ADSR envelope) is carried unchanged, since envelope shape is independent of where an event sits on the timeline.
  • reversed mirrors each event on [0, span]. Transforms may leave overlapping events; those are serialized at playback by mergeToSerial (highest intensity wins), not here. This is why the work sits on top of feat(core): serial-timeline waveform pipeline (overlap, fall-ramp, handle lifecycle) #85.
  • repeated folds over then, so repeated(n) equals then applied n times by construction.
  • A new internal spanMs() replaces the duplicated max-end-time calculation in rawSpanMs() and PatternElement.totalDurationMs. The SequenceElement/RepeatElement tree-sum overrides are scheduling math and stay as they are.

Tests

Property laws (kotest-property): reversed involution, timeStretch(1) identity and composition within a factor-derived tolerance, intensity range, then associativity and empty identity, span and count preservation. 138 tests pass on both iosSimulatorArm64 and the Android host.

Base: main.

Summary by CodeRabbit

  • New Features

    • Added new haptic pattern helpers to repeat, reverse, combine, scale intensity, and stretch timing.
    • You can now chain patterns together with either then or +.
    • Playback length and duration calculations were aligned to use a shared pattern span for more consistent results.
  • Bug Fixes

    • Improved handling of empty patterns and timeline calculations.
    • Added safeguards for invalid repeat, scaling, and stretch values.

@l2hyunwoo l2hyunwoo added the enhancement New feature or request label Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds pure extension functions on HapticPatternscaleIntensity, timeStretch, reversed, then, plus, and repeated — along with a shared internal spanMs() helper adopted by PatternElement and RawSpanMs. Includes property-based Kotest tests and updated public API dumps.

Changes

Pattern Algebra Transforms

Layer / File(s) Summary
Shared span computation
jindong-core/src/commonMain/.../model/PatternSpan.kt, .../element/PatternElement.kt, .../executor/RawSpanMs.kt
Introduces internal HapticPattern.spanMs() extension and refactors PatternElement.totalDurationMs and RawSpanMs.rawSpanMs to delegate to it.
then/plus concatenation
.../model/Then.kt, .../model/ThenTest.kt
Adds then infix function concatenating events by shifting timelines by spanMs(), with plus as an alias; tests verify associativity, identity, and span/count invariants.
repeated composition
.../model/Repeated.kt, .../model/PatternArb.kt, .../model/RepeatedTest.kt
Adds repeated(times) folding then over the pattern with validation; property tests use a new patterns() generator to check identity, empty result, and event count scaling.
reversed mirroring
.../model/Reversed.kt, .../model/ReversedTest.kt
Adds reversed() reflecting event start times across spanMs(); tests verify involution, span preservation, and duration multiset preservation.
scaleIntensity transform
.../model/ScaleIntensity.kt, .../model/ScaleIntensityTest.kt
Adds scaleIntensity(factor) converting intensities to scaled Custom values with validation; tests verify clamping to [0,1], identity at factor 1, and unchanged timing/iOS fields.
timeStretch transform
.../model/TimeStretch.kt, .../model/TimeStretchTest.kt
Adds timeStretch(factor) scaling event start/end edges with rounded duration recomputation; tests verify identity, non-negative durations, stretch composition, and span scaling tolerance.
API dumps
jindong-core/api/jindong-core.api, jindong-core/api/jindong-core.klib.api
Updates public API/ABI dumps to include the new repeated, reversed, scaleIntensity, then, plus, and timeStretch declarations.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant HapticPattern
  participant ThenFn as then/plus
  participant SpanMs as spanMs()

  Caller->>HapticPattern: repeated(times)
  loop times copies
    HapticPattern->>ThenFn: then(copy)
    ThenFn->>SpanMs: spanMs()
    SpanMs-->>ThenFn: current span
    ThenFn->>ThenFn: shift copy's events by span
    ThenFn-->>HapticPattern: concatenated pattern
  end
  HapticPattern-->>Caller: repeated pattern
Loading

Suggested reviewers: wisemuji

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding algebra-style transforms to HapticPattern.
Description check ✅ Passed The description includes the issue link, summary, implementation decisions, tests, and context, matching the template well.
Linked Issues check ✅ Passed The PR implements all requested HapticPattern transforms, shared span logic, and property tests from #88.
Out of Scope Changes check ✅ Passed The added files stay within the requested API, shared helper, tests, and apiDump regeneration.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pattern-algebra

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/ScaleIntensityTest.kt (1)

29-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a test for the negative-factor rejection path.

The require(factor >= 0f) guard in scaleIntensity is an important validation but has no corresponding test verifying that negative factors throw IllegalArgumentException. Adding a simple test would close this gap.

🧪 Suggested test
test("negative factor is rejected") {
  checkAll(patterns()) { pattern ->
    shouldThrow<IllegalArgumentException> {
      pattern.scaleIntensity(-0.5f)
    }
  }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/ScaleIntensityTest.kt`
around lines 29 - 59, Add a test in ScaleIntensityTest to cover the
negative-factor validation path in scaleIntensity. The current suite checks
clamping, identity scaling, and timing preservation, but not the require(factor
>= 0f) guard; add a new test that uses patterns() and asserts
pattern.scaleIntensity with a negative factor throws IllegalArgumentException,
using the scaleIntensity function as the anchor point.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/ScaleIntensityTest.kt`:
- Around line 29-59: Add a test in ScaleIntensityTest to cover the
negative-factor validation path in scaleIntensity. The current suite checks
clamping, identity scaling, and timing preservation, but not the require(factor
>= 0f) guard; add a new test that uses patterns() and asserts
pattern.scaleIntensity with a negative factor throws IllegalArgumentException,
using the scaleIntensity function as the anchor point.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 670fae3c-1874-42ea-a6ba-21d88df50510

📥 Commits

Reviewing files that changed from the base of the PR and between 7692779 and bec96b5.

📒 Files selected for processing (16)
  • jindong-core/api/jindong-core.api
  • jindong-core/api/jindong-core.klib.api
  • jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/element/PatternElement.kt
  • jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/executor/RawSpanMs.kt
  • jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/model/PatternSpan.kt
  • jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/model/Repeated.kt
  • jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/model/Reversed.kt
  • jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/model/ScaleIntensity.kt
  • jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/model/Then.kt
  • jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/model/TimeStretch.kt
  • jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/PatternArb.kt
  • jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/RepeatedTest.kt
  • jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/ReversedTest.kt
  • jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/ScaleIntensityTest.kt
  • jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/ThenTest.kt
  • jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/TimeStretchTest.kt

@l2hyunwoo
l2hyunwoo merged commit af42057 into main Jul 21, 2026
3 checks passed
@l2hyunwoo
l2hyunwoo deleted the feat/pattern-algebra branch July 21, 2026 05:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(core): add pattern algebra transforms on HapticPattern

1 participant