Skip to content

Commit 7ba5020

Browse files
committed
Improve angle/rotation casting
1 parent bc1f936 commit 7ba5020

6 files changed

Lines changed: 297 additions & 114 deletions

File tree

ShapeScript/Euclid+Extensions.swift

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -72,40 +72,29 @@ extension Angle {
7272

7373
extension Rotation {
7474
var rollYawPitchInHalfTurns: [Double] {
75-
[roll.radians / .pi, yaw.radians / .pi, pitch.radians / .pi]
75+
[roll.halfturns, yaw.halfturns, pitch.halfturns]
7676
}
7777

78-
init?(rollYawPitchInHalfTurns: [Double]) {
78+
init(rollYawPitchInHalfTurns: [Double]) {
7979
var roll = 0.0, yaw = 0.0, pitch = 0.0
8080
switch rollYawPitchInHalfTurns.count {
81-
case 3:
81+
case 3...:
8282
pitch = rollYawPitchInHalfTurns[2]
8383
fallthrough
8484
case 2:
8585
yaw = rollYawPitchInHalfTurns[1]
8686
fallthrough
8787
case 1:
8888
roll = rollYawPitchInHalfTurns[0]
89-
case 0:
90-
break
9189
default:
92-
return nil
90+
break
9391
}
9492
self.init(
95-
roll: .radians(roll * .pi),
96-
yaw: .radians(yaw * .pi),
97-
pitch: .radians(pitch * .pi)
93+
roll: .halfturns(roll),
94+
yaw: .halfturns(yaw),
95+
pitch: .halfturns(pitch)
9896
)
9997
}
100-
101-
init(unchecked rollYawPitchInHalfTurns: [Double]) {
102-
if let rotation = Rotation(rollYawPitchInHalfTurns: rollYawPitchInHalfTurns) {
103-
self = rotation
104-
} else {
105-
assertionFailure()
106-
self = .identity
107-
}
108-
}
10998
}
11099

111100
#if canImport(UIKit)

ShapeScript/Interpreter.swift

Lines changed: 60 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,9 +1282,21 @@ extension Expression {
12821282
case .minus:
12831283
switch value {
12841284
case let .tuple(values):
1285-
return .tuple(values.map { .number(-$0.doubleValue) })
1285+
return .tuple(values.map {
1286+
switch $0 {
1287+
case let .number(value):
1288+
return .number(-value)
1289+
case let .radians(value):
1290+
return .radians(-value)
1291+
default:
1292+
assertionFailure()
1293+
return value
1294+
}
1295+
})
12861296
case let .number(value):
12871297
return .number(-value)
1298+
case let .radians(value):
1299+
return .radians(-value)
12881300
default:
12891301
assertionFailure()
12901302
return value
@@ -1357,52 +1369,46 @@ extension Expression {
13571369
in: context
13581370
).boolValue)
13591371
case let .infix(lhs, op, rhs):
1360-
func doubleValue(_ exp: Expression, index: Int = 0) throws -> Double {
1361-
try exp.evaluate(
1362-
as: .number,
1363-
for: String(op.rawValue),
1364-
index: index,
1365-
in: context
1366-
).doubleValue
1372+
func evaluate(_ exp: Expression, as type: ValueType, at index: Int = 0) throws -> Value {
1373+
try exp.evaluate(as: type, for: op.rawValue, index: index, in: context)
13671374
}
1368-
func tupleValue(_ exp: Expression, index: Int = 0) throws -> Value {
1369-
try exp.evaluate(
1370-
as: .union([.number, .list(.number)]),
1371-
for: String(op.rawValue),
1372-
index: index,
1373-
in: context
1374-
)
1375+
func doubleValue(for exp: Expression, at index: Int = 0) throws -> Double {
1376+
try evaluate(exp, as: .number, at: index).doubleValue
13751377
}
1376-
func apply(
1377-
_ lhs: Value,
1378-
_ rhs: Value,
1379-
_ fn: (Double, Double) -> Double
1380-
) -> Value {
1381-
switch (lhs, rhs) {
1382-
case let (.number(lhs), .number(rhs)):
1383-
return .number(fn(lhs, rhs))
1384-
case let (.tuple(lhs), _) where lhs.count == 1:
1378+
func numberOrVectorValue(for exp: Expression, at index: Int = 0) throws -> Value {
1379+
try evaluate(exp, as: .numberOrVector, at: index)
1380+
}
1381+
func apply(_ lhs: Value, _ rhs: Value, _ fn: (Double, Double) -> Double) -> Value {
1382+
switch (lhs, rhs, op) {
1383+
case let (.number(lhs), .radians(rhs), .plus), let (.number(lhs), .radians(rhs), .minus),
1384+
let (.radians(lhs), .number(rhs), .plus), let (.radians(lhs), .number(rhs), .minus),
1385+
let (.radians(lhs), .radians(rhs), .times), // TODO: should this be an error?
1386+
let (.radians(lhs), .radians(rhs), .divide), // TODO: should this be halfTurns?
1387+
let (.number(lhs), .number(rhs), _):
1388+
return .number(fn(lhs, rhs)) // TODO: should this be an error?
1389+
case let (.number(lhs), .radians(rhs), .divide):
1390+
return .radians(fn(lhs, rhs)) // TODO: should this be a reciprocal radians type?
1391+
case let (.radians(lhs), .radians(rhs), _), let (.number(lhs), .radians(rhs), _),
1392+
let (.radians(lhs), .number(rhs), _):
1393+
return .radians(fn(lhs, rhs))
1394+
case let (.tuple(lhs), _, _) where lhs.count == 1:
13851395
return apply(lhs[0], rhs, fn)
1386-
case let (_, .tuple(rhs)) where rhs.count == 1:
1396+
case let (_, .tuple(rhs), _) where rhs.count == 1:
13871397
return apply(lhs, rhs[0], fn)
1388-
case let (.tuple(lhs), .tuple(rhs)):
1398+
case let (.tuple(lhs), .tuple(rhs), _):
13891399
return .tuple(zip(lhs, rhs).map { apply($0, $1, fn) })
1390-
case let (.tuple(lhs), .number):
1400+
case let (.tuple(lhs), .number, _):
13911401
return .tuple(lhs.map { apply($0, rhs, fn) })
1392-
case let (.number, .tuple(rhs)):
1402+
case let (.number, .tuple(rhs), _):
13931403
return .tuple(rhs.map { apply(lhs, $0, fn) })
13941404
default:
13951405
assertionFailure()
13961406
return .number(0)
13971407
}
13981408
}
1399-
func tupleApply(
1400-
_ fn: (Double, Double) -> Double,
1401-
lhs value: Value? = nil,
1402-
widen: Bool
1403-
) throws -> Value {
1404-
let lhs = try value ?? tupleValue(lhs)
1405-
let rhs = try tupleValue(rhs, index: 1)
1409+
func tupleApply(_ fn: (Double, Double) -> Double, widen: Bool) throws -> Value {
1410+
let lhs = try numberOrVectorValue(for: lhs)
1411+
let rhs = try numberOrVectorValue(for: rhs, at: 1)
14061412
switch (apply(lhs, rhs, fn), lhs) {
14071413
case let (.tuple(values), .tuple(lhs)) where widen:
14081414
return .tuple(values + lhs[values.count...])
@@ -1411,21 +1417,26 @@ extension Expression {
14111417
}
14121418
}
14131419
func tupleOrTextureApply(_ fn: (Double, Double) -> Double) throws -> Value {
1414-
let lhs = try lhs.evaluate(
1415-
as: .union([.number, .list(.number), .texture]),
1416-
for: String(op.rawValue),
1417-
index: 0,
1418-
in: context
1419-
)
1420-
if case let .texture(texture) = lhs {
1420+
let lhs = try evaluate(lhs, as: .union([
1421+
.number,
1422+
.radians,
1423+
.list(.number),
1424+
.list(.radians),
1425+
.texture,
1426+
]))
1427+
switch lhs {
1428+
case let .texture(texture):
14211429
guard let texture else {
14221430
return .texture(nil)
14231431
}
1424-
let rhs = try doubleValue(rhs)
1432+
let rhs = try doubleValue(for: rhs)
14251433
return .texture(texture.withIntensity(fn(texture.intensity, rhs)))
1434+
default:
1435+
let rhs = try numberOrVectorValue(for: rhs, at: 1)
1436+
return apply(lhs, rhs, fn)
14261437
}
1427-
return try tupleApply(fn, lhs: lhs, widen: false)
14281438
}
1439+
14291440
switch op {
14301441
case .minus:
14311442
return try tupleApply(-, widen: true)
@@ -1438,16 +1449,15 @@ extension Expression {
14381449
case .modulo:
14391450
return try tupleApply(fmod, widen: false)
14401451
case .lt:
1441-
return try .boolean(doubleValue(lhs) < doubleValue(rhs, index: 1))
1452+
return try .boolean(doubleValue(for: lhs) < doubleValue(for: rhs, at: 1))
14421453
case .gt:
1443-
return try .boolean(doubleValue(lhs) > doubleValue(rhs, index: 1))
1454+
return try .boolean(doubleValue(for: lhs) > doubleValue(for: rhs, at: 1))
14441455
case .lte:
1445-
return try .boolean(doubleValue(lhs) <= doubleValue(rhs, index: 1))
1456+
return try .boolean(doubleValue(for: lhs) <= doubleValue(for: rhs, at: 1))
14461457
case .gte:
1447-
return try .boolean(doubleValue(lhs) >= doubleValue(rhs, index: 1))
1458+
return try .boolean(doubleValue(for: lhs) >= doubleValue(for: rhs, at: 1))
14481459
case .in, .to, .step, .equal, .unequal, .and, .or:
1449-
throw RuntimeErrorType
1450-
.assertionFailure("\(op.rawValue) should be handled by earlier case")
1460+
throw RuntimeErrorType.assertionFailure("\(op.rawValue) should be handled by earlier case")
14511461
}
14521462
case let .member(expression, member):
14531463
let value = try expression.evaluate(in: context)

ShapeScript/StandardLibrary.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ extension Symbols {
541541
let values = value.doublesValue
542542
return .radians(atan2(values[0], values[1]))
543543
},
544-
"pi": .constant(.number(.pi)),
544+
"pi": .constant(.radians(.pi)),
545545
// Linear algebra
546546
"dot": .function(.tuple([.list(.number), .list(.number)]), .number) { value, _ in
547547
let values = value.tupleValue as! [[Double]]

ShapeScript/Types.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ extension ValueType {
103103
static let numberPair: ValueType = .tuple([.number, .number])
104104
static let colorOrTexture: ValueType = .union([.color, .texture])
105105
static let numberOrTexture: ValueType = .union([.number, .texture])
106-
static let numberOrVector: ValueType = .union([.number, .list(.number)])
106+
static let numberOrVector: ValueType = .union([.number, .radians, .list(.number), .list(.radians)])
107107

108108
static func optional(_ type: ValueType) -> ValueType {
109109
.union([type, .void])
@@ -508,7 +508,9 @@ extension Value {
508508
case let (.number(value), .size):
509509
return .size(Vector(size: value))
510510
case let (.number(value), .rotation):
511-
return .rotation(Rotation(unchecked: [value]))
511+
return .rotation(.roll(.halfturns(value)))
512+
case let (.halfturns(value), .rotation):
513+
return .rotation(.roll(.halfturns(value)))
512514
case let (.tuple(values), .tuple(types)):
513515
guard values.count == types.count else {
514516
return nil
@@ -547,20 +549,20 @@ extension Value {
547549
return numerify(values, range: 1 ... 3).map { .size(Vector(size: $0)) }
548550
case let (.tuple(values), .rotation):
549551
return numerify(values, as: .halfturns, range: 1 ... 3).map {
550-
.rotation(Rotation(unchecked: $0))
552+
.rotation(Rotation(rollYawPitchInHalfTurns: $0))
551553
}
552-
case let (.color(value), .list(.number)):
554+
case let (.color(value), .list(type)) where ValueType.number.isSubtype(of: type):
553555
return .tuple(value.components.map { .number($0) })
554-
case let (.vector(value), .list(.number)):
556+
case let (.vector(value), .list(type)) where ValueType.number.isSubtype(of: type):
555557
return .tuple(value.components.map { .number($0) })
556558
case let (.vector(value), .size):
557559
return .size(value)
558-
case let (.size(value), .list(.number)):
560+
case let (.size(value), .list(type)) where ValueType.number.isSubtype(of: type):
559561
return .tuple(value.components.map { .number($0) })
560562
case let (.size(value), .vector):
561563
return .vector(value)
562-
case let (.rotation(value), .list(.number)):
563-
return .tuple(value.rollYawPitchInHalfTurns.map { .number($0) })
564+
case let (.rotation(value), .list(type)) where ValueType.halfturns.isSubtype(of: type):
565+
return .tuple(value.rollYawPitchInHalfTurns.map { .halfturns($0) })
564566
case let (.object(values), .list(type)):
565567
let values = try values.sorted(by: { $0.0 < $1.0 }).compactMap {
566568
try Value(.string($0), $1).as(type, in: context)

ShapeScriptTests/InterpreterTests.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2526,6 +2526,38 @@ final class InterpreterTests: XCTestCase {
25262526
XCTAssertNoThrow(try evaluate(program, delegate: nil))
25272527
}
25282528

2529+
// MARK: Rotation / orientation
2530+
2531+
func testRotateWithHalfTurns() throws {
2532+
let program = """
2533+
cube {
2534+
orientation 1
2535+
print orientation
2536+
}
2537+
"""
2538+
let delegate = TestDelegate()
2539+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2540+
XCTAssertEqual(delegate.log, [Rotation.roll(.pi)])
2541+
}
2542+
2543+
func testRotateWithRadians() throws {
2544+
let program = """
2545+
cube {
2546+
orientation pi
2547+
print orientation
2548+
}
2549+
"""
2550+
XCTAssertThrowsError(try evaluate(parse(program), delegate: nil)) { error in
2551+
let error = try? XCTUnwrap(error as? RuntimeError)
2552+
XCTAssertEqual(error?.type, .typeMismatch(
2553+
for: "orientation",
2554+
index: -1,
2555+
expected: "rotation",
2556+
got: "angle in radians"
2557+
))
2558+
}
2559+
}
2560+
25292561
// MARK: Ranges
25302562

25312563
func testRange() {
@@ -3744,6 +3776,30 @@ final class InterpreterTests: XCTestCase {
37443776
XCTAssertEqual(delegate.log, [0, 1, 2, 1, -1, -2, -2, 0, -0.75])
37453777
}
37463778

3779+
func testTrigFunctions() {
3780+
let program = """
3781+
print acos(0)
3782+
print cos(pi)
3783+
print cos(pi * 2)
3784+
print sin(pi / 2)
3785+
"""
3786+
let delegate = TestDelegate()
3787+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
3788+
XCTAssertEqual(delegate.log, [Double.pi / 2, -1.0, 1.0, 1.0])
3789+
}
3790+
3791+
func testTrigWithNumericLiteral() {
3792+
let program = """
3793+
print cos(0)
3794+
// not clear we should allow this?
3795+
print sin(3.14159)
3796+
"""
3797+
let delegate = TestDelegate()
3798+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
3799+
XCTAssertEqual(delegate.log.first, 1.0)
3800+
XCTAssertEqual(try XCTUnwrap(delegate.log.last as? Double), 0, accuracy: 0.0001)
3801+
}
3802+
37473803
// MARK: Boolean algebra
37483804

37493805
func testLogicalAnd() {
@@ -4034,6 +4090,16 @@ final class InterpreterTests: XCTestCase {
40344090
XCTAssertEqual(delegate.log, [1.0, 4.0])
40354091
}
40364092

4093+
func testMixedVectorTypeMultiplication() {
4094+
let program = """
4095+
define axis 0 1 0
4096+
print axis * cube.bounds.size
4097+
"""
4098+
let delegate = TestDelegate()
4099+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
4100+
XCTAssertEqual(delegate.log, [0.0, 1.0, 0.0])
4101+
}
4102+
40374103
// MARK: Recursion
40384104

40394105
func testRecursiveLookupInBlockDefinition() {

0 commit comments

Comments
 (0)