Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,29 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
- uses: actions/setup-node@v6
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- name: Fetch frozen Rough.js oracle
run: |
curl --fail --location --silent --show-error \
https://registry.npmjs.org/roughjs/-/roughjs-4.0.4.tgz \
--output "$RUNNER_TEMP/roughjs-4.0.4.tgz"
mkdir "$RUNNER_TEMP/roughjs-4.0.4"
tar -xzf "$RUNNER_TEMP/roughjs-4.0.4.tgz" \
-C "$RUNNER_TEMP/roughjs-4.0.4"
echo "21f8682b994c6063e9605a19f50a6bd9f3b0901cbff4db2326b3f85308e29b7f $RUNNER_TEMP/roughjs-4.0.4/package/bundled/rough.js" \
https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz \
--output "$RUNNER_TEMP/roughjs-4.6.6.tgz"
echo "373873b2c3f1b7ddba878525c774816057c5818029780c7fba081f06018ba78c $RUNNER_TEMP/roughjs-4.6.6.tgz" \
| sha256sum --check
mkdir "$RUNNER_TEMP/roughjs-4.6.6"
tar -xzf "$RUNNER_TEMP/roughjs-4.6.6.tgz" \
-C "$RUNNER_TEMP/roughjs-4.6.6"
echo "6853af8619532f3939b156cd16824d2e14f31e71d3df306ad5558eeafacab194 $RUNNER_TEMP/roughjs-4.6.6/package/bundled/rough.js" \
| sha256sum --check
- name: Test
env:
ROUGH_GO_ROUGH_JS: ${{ runner.temp }}/roughjs-4.0.4/package/bundled/rough.js
ROUGH_GO_ROUGH_JS: ${{ runner.temp }}/roughjs-4.6.6/package/bundled/rough.js
ROUGH_GO_DIFFERENTIAL_CALLS: "1000"
run: go test ./...
- run: go test -race ./...
Expand Down
2 changes: 2 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
MIT License

Copyright (c) 2019 Preet Shihn
Copyright (c) 2020 Preet Shihn
Copyright (c) 2023 Preet Shihn
Copyright (c) 2026 rough-go contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
Expand Down
20 changes: 11 additions & 9 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
rough-go is a Go port of Rough.js 4.0.4.
rough-go implements the Rough.js 4.6.6 rendering surface used by D2.

Upstream source:
Project: Rough.js
Version: 4.0.4
Commit: 35302b8f1119ad004b4550f9a4d4162d3d845b0a
Version: 4.6.6
Commit: 56a2762171b1294d643501e8d14f120db6b27bd7
Repository: https://github.com/rough-stuff/rough
License: MIT
Copyright (c) 2019 Preet Shihn

Compatibility is measured against the Rough.js bundle embedded by D2, whose
SHA-256 is 5601a7c6046815613316d882ffbaede24261bfec295e3502ae7539abb1814820.
The pristine Rough.js 4.0.4 browser bundle has SHA-256
21f8682b994c6063e9605a19f50a6bd9f3b0901cbff4db2326b3f85308e29b7f.
The pristine Rough.js 4.6.6 browser bundle used as the test oracle has SHA-256
6853af8619532f3939b156cd16824d2e14f31e71d3df306ad5558eeafacab194.

No Rough.js JavaScript is linked into or executed by rough-go. The Go source is
a translation and remains available under the MIT license in LICENSE.
The port also derives implementation details from these Rough.js dependencies:
hachure-fill 0.5.2, Copyright (c) 2023 Preet Shihn, MIT
path-data-parser 0.1.0, Copyright (c) 2020 Preet Shihn, MIT

No Rough.js JavaScript is linked into or executed by rough-go at runtime. The Go
source is a translation and remains available under the MIT license in LICENSE.
32 changes: 18 additions & 14 deletions PROVENANCE.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
# Source provenance

rough-go is a source-shaped Go translation of Rough.js 4.0.4.
rough-go began as a source-shaped Go translation of Rough.js 4.0.4 and now
implements the Rough.js 4.6.6 rendering paths used by D2.

| Source | Version / commit | License | Relevant files |
| --- | --- | --- | --- |
| Rough.js | 4.0.4 / `35302b8f1119ad004b4550f9a4d4162d3d845b0a` | MIT | `src/core.ts`, `geometry.ts`, `math.ts`, `path.ts`, `renderer.ts`, `generator.ts`, `rough.ts`, `canvas.ts`, `svg.ts`, and `src/fillers/*.ts` |
| Rough.js | 4.6.6 / `56a2762171b1294d643501e8d14f120db6b27bd7` | MIT | `src/core.ts`, `geometry.ts`, `math.ts`, `renderer.ts`, `generator.ts`, and `src/fillers/*.ts` |
| Rough.js historical baseline | 4.0.4 / `35302b8f1119ad004b4550f9a4d4162d3d845b0a` | MIT | Original Go translation across the same generator and renderer packages |
| hachure-fill | 0.5.2 | MIT, Copyright (c) 2023 Preet Shihn | `filler_scan_line_hachure.go` |
| path-data-parser | 0.1.0 | MIT, Copyright (c) 2020 Preet Shihn | SVG path normalization and arc-to-cubic logic in `renderer.go` |

The pristine npm browser bundle has SHA-256
`21f8682b994c6063e9605a19f50a6bd9f3b0901cbff4db2326b3f85308e29b7f`.
The npm tarball has SHA-1 `31db3cc9654e7012a5d5701c6234f7601a14d720`
The pristine npm 4.6.6 browser bundle has SHA-256
`6853af8619532f3939b156cd16824d2e14f31e71d3df306ad5558eeafacab194`.
The npm tarball has SHA-256
`373873b2c3f1b7ddba878525c774816057c5818029780c7fba081f06018ba78c`,
SHA-1 `1059f49a5e0c80dee541a005b20cc322b222158b`,
and integrity
`sha512-rXmMGcALUlYIFKBbn9aWuxznPKOtnx9bouVC407/uneUNx0mT/4Mo2Z4TUieoCOT+rWmHnOQqVT1FvoN+L3baA==`.
`sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==`.

D2's frozen oracle bundle has SHA-256
`5601a7c6046815613316d882ffbaede24261bfec295e3502ae7539abb1814820`.
D2 reformatted and unwrapped the browser bundle, replaced deprecated regular
expression statics with local matches, and replaced exponentiation syntax with
`Math.pow`. Its later quoted-string typo correction is behaviorally inert. The
layout and renderer algorithms remain Rough.js 4.0.4.
CI accepts only that exact official browser bundle as its JavaScript oracle.
The release contract is the D2-used method and option surface documented in the
README, not every expert API exposed by Rough.js 4.6.6.

Every translated Go source file carries an SPDX MIT identifier and the upstream
copyright attribution. The complete applicable license is in `LICENSE`.
Every translated Go source file carries an SPDX MIT identifier and the relevant
upstream copyright attribution. The compatible MIT terms are reproduced in
`LICENSE`.
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# rough-go

`rough-go` is a native Go port of [Rough.js](https://roughjs.com/) 4.0.4. It
creates the same ordered drawing operations and SVG path descriptions without a
`rough-go` is a native Go compatibility port of the
[Rough.js](https://roughjs.com/) 4.6.6 rendering surface used by D2. It creates
the same ordered drawing operations and SVG path descriptions without a
JavaScript runtime.

The compatibility target is the exact Rough.js implementation embedded by D2:
version 4.0.4 at commit
`35302b8f1119ad004b4550f9a4d4162d3d845b0a`. Later Rough.js releases have
behavior-changing renderer and option changes, so they are not the oracle for
this module.
The compatibility target is Rough.js 4.6.6 at commit
`56a2762171b1294d643501e8d14f120db6b27bd7`. The port covers D2's generator
methods (`line`, `rectangle`, `ellipse`, `linearPath`, `polygon`, and SVG
`path`) and the solid and zigzag option profiles D2 emits. Other public methods
remain available for source compatibility, but arbitrary expert Rough.js
options outside that profile are not an exact compatibility contract.

```go
package main
Expand All @@ -31,10 +33,9 @@ func main() {
}
```

The full generator surface is ported: line, rectangle, ellipse, circle, linear
path, polygon, arc, curve, SVG path, every 4.0.4 fill style, operation
serialization, and `ToPaths` conversion. Pointer-valued option fields preserve
the JavaScript distinction between an omitted option and an explicit zero.
Pointer-valued option fields preserve the JavaScript distinction between an
omitted option and an explicit zero. Rough.js 4.6.6's new dash, multi-stroke,
vertex-preservation, fixed-decimal, and fill-roughness options are represented.

## Compatibility tests

Expand All @@ -46,17 +47,17 @@ go test -race ./...
go vet ./...
```

The optional differential suite runs the Go port against a frozen Rough.js
4.0.4 browser bundle. Node.js is used only by the test oracle; the library has
The optional differential suite runs the Go port against the pristine Rough.js
4.6.6 browser bundle. Node.js is used only by the test oracle; the library has
no JavaScript dependency.

```sh
ROUGH_GO_ROUGH_JS=/absolute/path/to/rough-4.0.4.js go test ./...
ROUGH_GO_ROUGH_JS=/absolute/path/to/rough-4.6.6.js go test ./...
```

Rough.js 4.0.4 did not ship automated tests. Accordingly, rough-go uses
source-shaped unit tests, deterministic randomized differential tests, and D2's
existing exact sketch SVG goldens.
The release gate includes 1,000 deterministic randomized D2-profile calls and
D2's full sketch SVG suite. Every rough-generated SVG path in that suite
matches official Rough.js 4.6.6 after D2's normal six-decimal serialization.

## License

Expand Down
167 changes: 111 additions & 56 deletions core.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Derived from Rough.js 4.0.4, Copyright (c) 2019 Preet Shihn.
// Derived from Rough.js 4.6.6, Copyright (c) 2019 Preet Shihn.

package rough

Expand Down Expand Up @@ -31,24 +31,33 @@ type Surface struct {
// Options contains optional Rough.js drawing options. Pointer fields preserve
// the distinction between an omitted option and an explicit zero value.
type Options struct {
MaxRandomnessOffset *float64 `json:"maxRandomnessOffset,omitempty"`
Roughness *float64 `json:"roughness,omitempty"`
Bowing *float64 `json:"bowing,omitempty"`
Stroke *string `json:"stroke,omitempty"`
StrokeWidth *float64 `json:"strokeWidth,omitempty"`
CurveFitting *float64 `json:"curveFitting,omitempty"`
CurveTightness *float64 `json:"curveTightness,omitempty"`
CurveStepCount *float64 `json:"curveStepCount,omitempty"`
Fill *string `json:"fill,omitempty"`
FillStyle *string `json:"fillStyle,omitempty"`
FillWeight *float64 `json:"fillWeight,omitempty"`
HachureAngle *float64 `json:"hachureAngle,omitempty"`
HachureGap *float64 `json:"hachureGap,omitempty"`
Simplification *float64 `json:"simplification,omitempty"`
DashOffset *float64 `json:"dashOffset,omitempty"`
DashGap *float64 `json:"dashGap,omitempty"`
ZigzagOffset *float64 `json:"zigzagOffset,omitempty"`
Seed *float64 `json:"seed,omitempty"`
MaxRandomnessOffset *float64 `json:"maxRandomnessOffset,omitempty"`
Roughness *float64 `json:"roughness,omitempty"`
Bowing *float64 `json:"bowing,omitempty"`
Stroke *string `json:"stroke,omitempty"`
StrokeWidth *float64 `json:"strokeWidth,omitempty"`
CurveFitting *float64 `json:"curveFitting,omitempty"`
CurveTightness *float64 `json:"curveTightness,omitempty"`
CurveStepCount *float64 `json:"curveStepCount,omitempty"`
Fill *string `json:"fill,omitempty"`
FillStyle *string `json:"fillStyle,omitempty"`
FillWeight *float64 `json:"fillWeight,omitempty"`
HachureAngle *float64 `json:"hachureAngle,omitempty"`
HachureGap *float64 `json:"hachureGap,omitempty"`
Simplification *float64 `json:"simplification,omitempty"`
DashOffset *float64 `json:"dashOffset,omitempty"`
DashGap *float64 `json:"dashGap,omitempty"`
ZigzagOffset *float64 `json:"zigzagOffset,omitempty"`
Seed *float64 `json:"seed,omitempty"`
StrokeLineDash []float64 `json:"strokeLineDash,omitempty"`
StrokeLineDashOffset *float64 `json:"strokeLineDashOffset,omitempty"`
FillLineDash []float64 `json:"fillLineDash,omitempty"`
FillLineDashOffset *float64 `json:"fillLineDashOffset,omitempty"`
DisableMultiStroke *bool `json:"disableMultiStroke,omitempty"`
DisableMultiStrokeFill *bool `json:"disableMultiStrokeFill,omitempty"`
PreserveVertices *bool `json:"preserveVertices,omitempty"`
FixedDecimalPlaceDigits *float64 `json:"fixedDecimalPlaceDigits,omitempty"`
FillShapeRoughnessGain *float64 `json:"fillShapeRoughnessGain,omitempty"`
}

// Float64 returns a pointer suitable for a numeric Options field.
Expand All @@ -57,28 +66,42 @@ func Float64(v float64) *float64 { return &v }
// String returns a pointer suitable for a string Options field.
func String(v string) *string { return &v }

// Bool returns a pointer suitable for a boolean Options field.
func Bool(v bool) *bool { return &v }

// ResolvedOptions is the complete option set used by the renderer.
type ResolvedOptions struct {
MaxRandomnessOffset float64
Roughness float64
Bowing float64
Stroke string
StrokeWidth float64
CurveFitting float64
CurveTightness float64
CurveStepCount float64
Fill string
FillStyle string
FillWeight float64
HachureAngle float64
HachureGap float64
Simplification float64
DashOffset float64
DashGap float64
ZigzagOffset float64
Seed float64
RoughnessGain float64
randomizer *randomState
MaxRandomnessOffset float64
Roughness float64
Bowing float64
Stroke string
StrokeWidth float64
CurveFitting float64
CurveTightness float64
CurveStepCount float64
Fill string
FillStyle string
FillWeight float64
HachureAngle float64
HachureGap float64
Simplification float64
DashOffset float64
DashGap float64
ZigzagOffset float64
Seed float64
StrokeLineDash []float64
StrokeLineDashOffset float64
FillLineDash []float64
FillLineDashOffset float64
DisableMultiStroke bool
DisableMultiStrokeFill bool
PreserveVertices bool
FixedDecimalPlaceDigits *float64
FillShapeRoughnessGain float64
// RoughnessGain is retained for source compatibility with v0.1.0. Rough.js
// 4.6.6 computes the gain per line and no longer mutates this field.
RoughnessGain float64
randomizer *randomState
}

// OpType identifies a primitive path operation.
Expand Down Expand Up @@ -145,23 +168,27 @@ type PatternInfo struct {

func defaultResolvedOptions() ResolvedOptions {
return ResolvedOptions{
MaxRandomnessOffset: 2,
Roughness: 1,
Bowing: 1,
Stroke: "#000",
StrokeWidth: 1,
CurveFitting: 0.95,
CurveTightness: 0,
CurveStepCount: 9,
FillStyle: "hachure",
FillWeight: -1,
HachureAngle: -41,
HachureGap: -1,
DashOffset: -1,
DashGap: -1,
ZigzagOffset: -1,
Seed: 0,
RoughnessGain: 1,
MaxRandomnessOffset: 2,
Roughness: 1,
Bowing: 1,
Stroke: "#000",
StrokeWidth: 1,
CurveFitting: 0.95,
CurveTightness: 0,
CurveStepCount: 9,
FillStyle: "hachure",
FillWeight: -1,
HachureAngle: -41,
HachureGap: -1,
DashOffset: -1,
DashGap: -1,
ZigzagOffset: -1,
Seed: 0,
DisableMultiStroke: false,
DisableMultiStrokeFill: false,
PreserveVertices: false,
FillShapeRoughnessGain: 0.8,
RoughnessGain: 1,
}
}

Expand Down Expand Up @@ -223,5 +250,33 @@ func mergeOptions(base ResolvedOptions, o *Options) ResolvedOptions {
if o.Seed != nil {
base.Seed = *o.Seed
}
if o.StrokeLineDash != nil {
base.StrokeLineDash = append([]float64(nil), o.StrokeLineDash...)
}
if o.StrokeLineDashOffset != nil {
base.StrokeLineDashOffset = *o.StrokeLineDashOffset
}
if o.FillLineDash != nil {
base.FillLineDash = append([]float64(nil), o.FillLineDash...)
}
if o.FillLineDashOffset != nil {
base.FillLineDashOffset = *o.FillLineDashOffset
}
if o.DisableMultiStroke != nil {
base.DisableMultiStroke = *o.DisableMultiStroke
}
if o.DisableMultiStrokeFill != nil {
base.DisableMultiStrokeFill = *o.DisableMultiStrokeFill
}
if o.PreserveVertices != nil {
base.PreserveVertices = *o.PreserveVertices
}
if o.FixedDecimalPlaceDigits != nil {
v := *o.FixedDecimalPlaceDigits
base.FixedDecimalPlaceDigits = &v
}
if o.FillShapeRoughnessGain != nil {
base.FillShapeRoughnessGain = *o.FillShapeRoughnessGain
}
return base
}
Loading