Skip to content

Commit 679106d

Browse files
committed
Merge branch 'master' into feature/polygon-masks
# Conflicts: # src/common/geometry.spec.js # src/features/layers/layersSlice.js # src/features/machines/PolarMachine.js
2 parents 65bf661 + f7177d6 commit 679106d

40 files changed

Lines changed: 5975 additions & 607 deletions

package-lock.json

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "sandify",
33
"homepage": "https://sandify.org",
4-
"version": "1.1.1",
4+
"version": "1.2.2",
55
"private": true,
66
"dependencies": {
77
"@dnd-kit/core": "^6.3.1",
@@ -59,6 +59,7 @@
5959
"reselect": "^5.1.1",
6060
"sass": "^1.96.0",
6161
"seedrandom": "^3.0.5",
62+
"svgpath": "^2.6.0",
6263
"uuid": "^13.0.0",
6364
"victor": "^1.1.0"
6465
},

src/common/colors.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,61 @@ const colors = {
1010
transformerBorderColor: "#fefefe", // almost white
1111
}
1212

13+
// ITU-R BT.601 luminance formula for perceived brightness
14+
const luminance = (r, g, b) => 0.299 * r + 0.587 * g + 0.114 * b
15+
16+
// Parse a CSS color string and return perceived brightness (0-255), or null if unparseable
17+
export const getColorBrightness = (color) => {
18+
if (!color || color === "none") {
19+
return null
20+
}
21+
22+
const c = color.toLowerCase().trim()
23+
24+
if (c.startsWith("#")) {
25+
let hex = c.slice(1)
26+
27+
if (hex.length === 3) {
28+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
29+
}
30+
31+
if (hex.length === 6 && /^[0-9a-f]{6}$/.test(hex)) {
32+
const r = parseInt(hex.slice(0, 2), 16)
33+
const g = parseInt(hex.slice(2, 4), 16)
34+
const b = parseInt(hex.slice(4, 6), 16)
35+
36+
return luminance(r, g, b)
37+
}
38+
}
39+
40+
const rgbMatch = c.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/)
41+
42+
if (rgbMatch) {
43+
const r = parseInt(rgbMatch[1], 10)
44+
const g = parseInt(rgbMatch[2], 10)
45+
const b = parseInt(rgbMatch[3], 10)
46+
47+
return luminance(r, g, b)
48+
}
49+
50+
const namedColors = {
51+
black: 0,
52+
white: 255,
53+
red: 76,
54+
green: 150,
55+
blue: 29,
56+
yellow: 226,
57+
orange: 156,
58+
brown: 101,
59+
gray: 128,
60+
grey: 128,
61+
}
62+
63+
if (namedColors[c] !== undefined) {
64+
return namedColors[c]
65+
}
66+
67+
return null
68+
}
69+
1370
export default colors

src/common/colors.spec.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { getColorBrightness } from "./colors"
2+
3+
describe("getColorBrightness", () => {
4+
describe("hex colors", () => {
5+
it("returns 0 for black", () => {
6+
expect(getColorBrightness("#000000")).toBe(0)
7+
})
8+
9+
it("returns 255 for white", () => {
10+
expect(getColorBrightness("#ffffff")).toBe(255)
11+
})
12+
13+
it("handles 3-digit hex", () => {
14+
expect(getColorBrightness("#000")).toBe(0)
15+
expect(getColorBrightness("#fff")).toBe(255)
16+
})
17+
18+
it("calculates luminance for red", () => {
19+
// Red: 0.299 * 255 = 76.245
20+
expect(getColorBrightness("#ff0000")).toBeCloseTo(76.245)
21+
})
22+
23+
it("calculates luminance for green", () => {
24+
// Green: 0.587 * 255 = 149.685
25+
expect(getColorBrightness("#00ff00")).toBeCloseTo(149.685)
26+
})
27+
28+
it("calculates luminance for blue", () => {
29+
// Blue: 0.114 * 255 = 29.07
30+
expect(getColorBrightness("#0000ff")).toBeCloseTo(29.07)
31+
})
32+
33+
it("is case insensitive", () => {
34+
expect(getColorBrightness("#FFFFFF")).toBe(255)
35+
expect(getColorBrightness("#FfFfFf")).toBe(255)
36+
})
37+
})
38+
39+
describe("rgb/rgba colors", () => {
40+
it("parses rgb format", () => {
41+
expect(getColorBrightness("rgb(255, 255, 255)")).toBe(255)
42+
expect(getColorBrightness("rgb(0, 0, 0)")).toBe(0)
43+
})
44+
45+
it("parses rgba format", () => {
46+
expect(getColorBrightness("rgba(255, 255, 255, 0.5)")).toBe(255)
47+
})
48+
49+
it("handles spaces in rgb", () => {
50+
expect(getColorBrightness("rgb( 255 , 255 , 255 )")).toBe(255)
51+
})
52+
})
53+
54+
describe("named colors", () => {
55+
it("returns correct brightness for named colors", () => {
56+
expect(getColorBrightness("black")).toBe(0)
57+
expect(getColorBrightness("white")).toBe(255)
58+
expect(getColorBrightness("gray")).toBe(128)
59+
expect(getColorBrightness("grey")).toBe(128)
60+
})
61+
62+
it("is case insensitive", () => {
63+
expect(getColorBrightness("BLACK")).toBe(0)
64+
expect(getColorBrightness("White")).toBe(255)
65+
})
66+
})
67+
68+
describe("invalid/special values", () => {
69+
it("returns null for 'none'", () => {
70+
expect(getColorBrightness("none")).toBeNull()
71+
})
72+
73+
it("returns null for empty string", () => {
74+
expect(getColorBrightness("")).toBeNull()
75+
})
76+
77+
it("returns null for null input", () => {
78+
expect(getColorBrightness(null)).toBeNull()
79+
})
80+
81+
it("returns null for undefined input", () => {
82+
expect(getColorBrightness(undefined)).toBeNull()
83+
})
84+
85+
it("returns null for unrecognized color names", () => {
86+
expect(getColorBrightness("hotpink")).toBeNull()
87+
expect(getColorBrightness("notacolor")).toBeNull()
88+
})
89+
90+
it("returns null for invalid hex", () => {
91+
expect(getColorBrightness("#gggggg")).toBeNull()
92+
expect(getColorBrightness("#12345")).toBeNull()
93+
})
94+
})
95+
96+
describe("whitespace handling", () => {
97+
it("trims whitespace", () => {
98+
expect(getColorBrightness(" #ffffff ")).toBe(255)
99+
expect(getColorBrightness(" black ")).toBe(0)
100+
})
101+
})
102+
})

src/common/geometry.js

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ export const snapToGrid = (value, tolerance) => {
1616
return Math.round(value / tolerance) * tolerance
1717
}
1818

19-
export const distance = (v1, v2) => {
20-
return Math.sqrt(Math.pow(v1.x - v2.x, 2.0) + Math.pow(v1.y - v2.y, 2.0))
21-
}
19+
// Using x*x instead of Math.pow(x, 2) avoids function call overhead (~10-20x
20+
// faster for squaring).
21+
export const magnitude = (x, y) => Math.sqrt(x * x + y * y)
22+
23+
export const distance = (v1, v2) => magnitude(v1.x - v2.x, v1.y - v2.y)
2224

2325
// Calculate the centroid (geometric center) of a set of vertices.
2426
// Excludes duplicate closing vertex if present (would skew the average).
@@ -253,6 +255,27 @@ export const toWorldSpace = (vertex, x, y, rotation) => {
253255
return offset(rotate(vertex, -rotation), x, y)
254256
}
255257

258+
// applies a DOMMatrix (or object with a,b,c,d,e,f) to a vertex
259+
// | a c e | | x | | a*x + c*y + e |
260+
// | b d f | × | y | = | b*x + d*y + f |
261+
export const applyMatrix = (vertex, matrix) => {
262+
const { a, b, c, d, e, f } = matrix
263+
const newX = a * vertex.x + c * vertex.y + e
264+
const newY = b * vertex.x + d * vertex.y + f
265+
266+
vertex.x = newX
267+
vertex.y = newY
268+
269+
return vertex
270+
}
271+
272+
// applies a matrix to an array of vertices
273+
export const applyMatrixToVertices = (vertices, matrix) => {
274+
vertices.forEach((vertex) => applyMatrix(vertex, matrix))
275+
276+
return vertices
277+
}
278+
256279
// modifies the given array in place, centering the points on (0, 0)
257280
export const centerOnOrigin = (vertices, bounds) => {
258281
if (vertices.length === 0) return vertices
@@ -326,6 +349,37 @@ export const circle = (radius, start = 0, x = 0, y = 0, resolution = 128.0) => {
326349
return points
327350
}
328351

352+
// returns an array of points drawing an ellipse with given radii
353+
export const ellipse = (rx, ry, cx = 0, cy = 0, resolution = 128.0) => {
354+
return ellipticalArc(rx, ry, 0, Math.PI * 2, cx, cy, resolution / 4)
355+
}
356+
357+
// returns an array of points drawing an elliptical arc
358+
export const ellipticalArc = (
359+
rx,
360+
ry,
361+
startAngle,
362+
endAngle,
363+
cx = 0,
364+
cy = 0,
365+
resolution = 16,
366+
) => {
367+
const steps = Math.max(
368+
4,
369+
Math.ceil((resolution * Math.abs(endAngle - startAngle)) / (Math.PI / 2)),
370+
)
371+
const points = []
372+
373+
for (let i = 0; i <= steps; i++) {
374+
const angle = startAngle + ((endAngle - startAngle) * i) / steps
375+
points.push(
376+
new Victor(cx + Math.cos(angle) * rx, cy + Math.sin(angle) * ry),
377+
)
378+
}
379+
380+
return points
381+
}
382+
329383
export const arc = (
330384
radius,
331385
startAngle,

0 commit comments

Comments
 (0)