Give machine props world-space 3D form that rotates with the camera
Scene shapes, cable anchors, bar angles, pad perpendiculars, and roller offsets all resolve in the authored view exactly as before, then rotate about the world-vertical axis through the root anchor - the same resolve-then-rotate pattern as the figure's pins and the mat - so at the authored yaw every exercise renders bit-identically to today, and under an orbiting camera the equipment turns with the figure while staying welded to its hands and feet. Scene lines gain an optional depth plane (z) and slab extrusion (depth) so seats, backrests, and platforms keep form edge-on; the rect shape is retired (re-authored as slab lines). All 14 machines' props re-authored with depths and verified at eight orbit angles. The fixture snapshots move into the pipeline as render.py --fixtures and now cover orbit-presentation samples with resolved prop primitives for a spread of prop flavors; the in-app renderer resolves props in MotionSolver (lockstep with resolve_props) and the view just draws primitives. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
This commit is contained in:
@@ -46,11 +46,12 @@ struct FigureAnimation {
|
||||
/// describes one authored viewpoint, so machines keep their fixed camera.
|
||||
var orbits: Bool { props.isEmpty && hide.isEmpty }
|
||||
|
||||
/// Drawable geometry at `time`, with limbs listed in `hide` dropped so neither they
|
||||
/// nor any prop riding them are drawn.
|
||||
/// Drawable geometry at `time` — the figure plus the resolved equipment layers —
|
||||
/// with limbs listed in `hide` dropped so neither they nor any prop riding them
|
||||
/// are drawn.
|
||||
func geometry(at time: Double) -> FigureGeometry {
|
||||
let yaw = orbits ? 360 * (time / Self.orbitPeriod) : 0
|
||||
var geo = timeline.geometry(at: time, yawOffset: yaw)
|
||||
var geo = timeline.geometry(at: time, yawOffset: yaw, props: props)
|
||||
for name in hide {
|
||||
if let limb = FigureLimb(rawValue: name) { geo.limbs[limb] = nil }
|
||||
}
|
||||
@@ -121,14 +122,14 @@ struct ExerciseFigureView: View {
|
||||
}
|
||||
|
||||
// Equipment behind the figure: scene shapes and cables.
|
||||
drawBackgroundProps(&ctx, geo)
|
||||
drawProps(&ctx, geo.propsBackground)
|
||||
|
||||
// Parts far-to-near. The head paints last (opaque), preceded by the joint-attached
|
||||
// equipment so bars/pads sit over the limbs but behind the face.
|
||||
for part in geo.order {
|
||||
switch part {
|
||||
case "head":
|
||||
drawAttachedProps(&ctx, geo)
|
||||
drawProps(&ctx, geo.propsForeground)
|
||||
drawHead(&ctx, geo)
|
||||
case "spine":
|
||||
drawSpine(&ctx, geo)
|
||||
@@ -164,132 +165,39 @@ struct ExerciseFigureView: View {
|
||||
|
||||
// MARK: Props
|
||||
|
||||
/// Prop joint refs → (limb, chain index): extremities are index 2, mid joints
|
||||
/// (elbows/knees) index 1, so equipment can ride either joint. Kept 1:1 with
|
||||
/// the reference renderer's `JOINT_LIMB` (legs now end in a foot bone, so a foot
|
||||
/// prop rides the ankle at index 2).
|
||||
private static let propJoints: [String: (limb: FigureLimb, index: Int)] = [
|
||||
"hand_r": (.armR, 2), "elbow_r": (.armR, 1),
|
||||
"hand_l": (.armL, 2), "elbow_l": (.armL, 1),
|
||||
"foot_r": (.legR, 2), "knee_r": (.legR, 1),
|
||||
"foot_l": (.legL, 2), "knee_l": (.legL, 1),
|
||||
]
|
||||
|
||||
/// The joint point a prop follows (plus the unit direction of the bone ending
|
||||
/// at the first joint, for perpendicular items). Nil when any referenced limb
|
||||
/// isn't drawn this frame.
|
||||
private func jointAnchor(_ geo: FigureGeometry, _ ref: PropJointRef) -> (point: CGPoint, direction: CGVector)? {
|
||||
var points: [CGPoint] = []
|
||||
var direction: CGVector?
|
||||
for name in ref.names {
|
||||
guard let joint = Self.propJoints[name],
|
||||
let chain = geo.limbs[joint.limb], chain.count > joint.index else { return nil }
|
||||
let a = chain[joint.index - 1]
|
||||
let b = chain[joint.index]
|
||||
points.append(b)
|
||||
if direction == nil {
|
||||
let d = max(hypot(b.x - a.x, b.y - a.y), 1)
|
||||
direction = CGVector(dx: (b.x - a.x) / d, dy: (b.y - a.y) / d)
|
||||
}
|
||||
}
|
||||
guard let direction, !points.isEmpty else { return nil }
|
||||
let center = CGPoint(x: points.reduce(0) { $0 + $1.x } / CGFloat(points.count),
|
||||
y: points.reduce(0) { $0 + $1.y } / CGFloat(points.count))
|
||||
return (center, direction)
|
||||
}
|
||||
|
||||
private func drawBackgroundProps(_ ctx: inout GraphicsContext, _ geo: FigureGeometry) {
|
||||
for prop in figure.props {
|
||||
switch prop.type {
|
||||
case "scene":
|
||||
for shape in prop.shapes ?? [] { drawSceneShape(&ctx, shape) }
|
||||
case "cable":
|
||||
guard let from = prop.from, from.count == 2, let to = prop.to,
|
||||
let anchor = jointAnchor(geo, to) else { continue }
|
||||
stroke(&ctx, [CGPoint(x: from[0], y: from[1]), anchor.point],
|
||||
color: .figureEquipment, width: prop.w ?? 2)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bars sit at a fixed world angle (default horizontal); dumbbells and pads
|
||||
/// default to perpendicular to the lower bone. Kept 1:1 with the reference
|
||||
/// renderer's `resolve_props` — change them in lockstep.
|
||||
private func drawAttachedProps(_ ctx: inout GraphicsContext, _ geo: FigureGeometry) {
|
||||
for prop in figure.props {
|
||||
if prop.type == "roller" {
|
||||
drawRoller(&ctx, geo, prop)
|
||||
continue
|
||||
}
|
||||
let defaults: (halfLen: Double, width: Double, plateR: Double)
|
||||
switch prop.type {
|
||||
case "bar": defaults = (24, 4, 0)
|
||||
case "dumbbell": defaults = (7, 3, 4.5)
|
||||
case "pad": defaults = (8, 7, 0)
|
||||
default: continue
|
||||
}
|
||||
guard let at = prop.at, let anchor = jointAnchor(geo, at) else { continue }
|
||||
let axis: CGVector
|
||||
if prop.type == "bar" || prop.angle != nil {
|
||||
axis = MotionSolver.direction(prop.angle ?? 0)
|
||||
} else {
|
||||
axis = CGVector(dx: -anchor.direction.dy, dy: anchor.direction.dx)
|
||||
}
|
||||
let h = prop.halfLen ?? defaults.halfLen
|
||||
let a = CGPoint(x: anchor.point.x - axis.dx * h, y: anchor.point.y - axis.dy * h)
|
||||
let b = CGPoint(x: anchor.point.x + axis.dx * h, y: anchor.point.y + axis.dy * h)
|
||||
stroke(&ctx, [a, b], color: .figureProp, width: prop.w ?? defaults.width)
|
||||
let plateR = prop.plateR ?? defaults.plateR
|
||||
if plateR > 0 {
|
||||
for end in [a, b] {
|
||||
let rect = CGRect(x: end.x - plateR, y: end.y - plateR,
|
||||
width: 2 * plateR, height: 2 * plateR)
|
||||
ctx.fill(Path(ellipseIn: rect), with: .color(.figureProp))
|
||||
/// Draws one resolved equipment layer. The solver hands back ready canvas
|
||||
/// primitives (`MotionSolver.resolveProps`, kept 1:1 with the reference
|
||||
/// renderer); the view only maps inks to the adaptive palette.
|
||||
private func drawProps(_ ctx: inout GraphicsContext, _ prims: [PropPrimitive]) {
|
||||
for prim in prims {
|
||||
switch prim {
|
||||
case .line(let points, let width, let ink):
|
||||
stroke(&ctx, points, color: color(for: ink), width: width)
|
||||
case .poly(let points, let width, let ink):
|
||||
// An extruded scene slab: filled and outlined, so it degenerates
|
||||
// to the plain line whenever the sweep collapses edge-on.
|
||||
guard let first = points.first else { continue }
|
||||
var path = Path()
|
||||
path.addLines(points)
|
||||
path.addLine(to: first)
|
||||
path.closeSubpath()
|
||||
ctx.fill(path, with: .color(color(for: ink)))
|
||||
ctx.stroke(path, with: .color(color(for: ink)),
|
||||
style: StrokeStyle(lineWidth: width, lineCap: .round, lineJoin: .round))
|
||||
case .circle(let center, let radius, let width, let fill, let ink):
|
||||
let rect = CGRect(x: center.x - radius, y: center.y - radius,
|
||||
width: 2 * radius, height: 2 * radius)
|
||||
if fill {
|
||||
ctx.fill(Path(ellipseIn: rect), with: .color(color(for: ink)))
|
||||
} else {
|
||||
ctx.stroke(Path(ellipseIn: rect), with: .color(color(for: ink)), lineWidth: width)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A machine roller pad seen end-on: a disc riding the limb's lower bone near the
|
||||
/// joint, on the `side` (+1/−1) of the bone it presses. Kept 1:1 with the reference
|
||||
/// renderer's `resolve_props`.
|
||||
private func drawRoller(_ ctx: inout GraphicsContext, _ geo: FigureGeometry, _ prop: MotionProp) {
|
||||
guard let at = prop.at, let anchor = jointAnchor(geo, at) else { return }
|
||||
let r = prop.r ?? 5
|
||||
let back = prop.back ?? 0
|
||||
let side = prop.side ?? 1
|
||||
let px = anchor.direction.dy * side, py = -anchor.direction.dx * side
|
||||
let center = CGPoint(x: anchor.point.x - anchor.direction.dx * back + px * (r + 3),
|
||||
y: anchor.point.y - anchor.direction.dy * back + py * (r + 3))
|
||||
let rect = CGRect(x: center.x - r, y: center.y - r, width: 2 * r, height: 2 * r)
|
||||
ctx.fill(Path(ellipseIn: rect), with: .color(.figureProp))
|
||||
}
|
||||
|
||||
private func drawSceneShape(_ ctx: inout GraphicsContext, _ shape: PropSceneShape) {
|
||||
let color: Color = shape.color == "prop" ? .figureProp : .figureEquipment
|
||||
switch shape.kind {
|
||||
case "line":
|
||||
guard let pts = shape.pts else { return }
|
||||
stroke(&ctx, pts.compactMap { $0.count == 2 ? CGPoint(x: $0[0], y: $0[1]) : nil },
|
||||
color: color, width: shape.w ?? 4)
|
||||
case "circle":
|
||||
guard let c = shape.c, c.count == 2, let r = shape.r else { return }
|
||||
let rect = CGRect(x: c[0] - r, y: c[1] - r, width: 2 * r, height: 2 * r)
|
||||
if shape.fill ?? true {
|
||||
ctx.fill(Path(ellipseIn: rect), with: .color(color))
|
||||
} else {
|
||||
ctx.stroke(Path(ellipseIn: rect), with: .color(color), lineWidth: shape.w ?? 3)
|
||||
}
|
||||
case "rect":
|
||||
guard let x = shape.x, let y = shape.y, let w = shape.w, let h = shape.h else { return }
|
||||
let path = Path(roundedRect: CGRect(x: x, y: y, width: w, height: h),
|
||||
cornerRadius: shape.r ?? 2)
|
||||
ctx.fill(path, with: .color(color))
|
||||
default:
|
||||
break
|
||||
}
|
||||
private func color(for ink: PropInk) -> Color {
|
||||
ink == .prop ? .figureProp : .figureEquipment
|
||||
}
|
||||
|
||||
/// Near parts take the dark ink, far parts the light one; working parts swap for the
|
||||
|
||||
@@ -140,20 +140,22 @@ enum PropJointRef: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
/// One static shape of a `scene` prop, in canvas coordinates.
|
||||
/// One static shape of a `scene` prop, authored in canvas coordinates in the
|
||||
/// authored view, with world-space 3D form for the orbiting presentation.
|
||||
struct PropSceneShape: Codable {
|
||||
let kind: String // "line" | "circle" | "rect"
|
||||
/// line: polyline points; `w` is the stroke width.
|
||||
let kind: String // "line" | "circle"
|
||||
/// line: polyline points, `[x, y]` or `[x, y, z]`; `w` is the stroke width.
|
||||
let pts: [[Double]]?
|
||||
let w: Double?
|
||||
/// circle: center + radius; `fill` false draws an outline.
|
||||
let c: [Double]?
|
||||
let r: Double?
|
||||
let fill: Bool?
|
||||
/// rect: origin + size (`w`/`h`), corner radius `r`; always filled.
|
||||
let x: Double?
|
||||
let y: Double?
|
||||
let h: Double?
|
||||
/// The shape's depth plane (+ toward the camera in the authored view).
|
||||
let z: Double?
|
||||
/// Extrusion half-width: a line with `depth` is a slab (a seat, a platform)
|
||||
/// that opens into a swept quad as the camera orbits.
|
||||
let depth: Double?
|
||||
/// Optional palette override: `"prop"` for the darker attached-item gray.
|
||||
let color: String?
|
||||
}
|
||||
@@ -164,7 +166,7 @@ struct MotionProp: Codable {
|
||||
let type: String // "scene" | "cable" | "bar" | "dumbbell" | "pad" | "roller"
|
||||
/// scene: the static shapes.
|
||||
let shapes: [PropSceneShape]?
|
||||
/// cable: fixed anchor `[x, y]` → moving joint `to`.
|
||||
/// cable: fixed anchor `[x, y]` or `[x, y, z]` → moving joint `to`.
|
||||
let from: [Double]?
|
||||
let to: PropJointRef?
|
||||
/// bar/dumbbell/pad: the joint(s) the item is centered on.
|
||||
|
||||
@@ -150,6 +150,25 @@ struct NormalizedFrame {
|
||||
/// the far member draws light, behind, and nudged by the readability offset.
|
||||
enum Shade { case near, far }
|
||||
|
||||
/// The two equipment inks: recessive `equipment` gray for scene shapes and cables
|
||||
/// behind the figure, darker `prop` gray for joint-attached items over the limbs.
|
||||
enum PropInk {
|
||||
case equipment, prop
|
||||
|
||||
init(_ name: String?) { self = name == "prop" ? .prop : .equipment }
|
||||
}
|
||||
|
||||
/// One resolved equipment drawing primitive, in canvas coordinates — the shape the
|
||||
/// prop layer reduces to for a single frame (mirroring the reference renderer's
|
||||
/// `resolve_props` output; kept 1:1, change them in lockstep).
|
||||
enum PropPrimitive {
|
||||
case line(points: [CGPoint], width: Double, ink: PropInk)
|
||||
/// A closed, filled, outlined polygon — an extruded scene slab. Degenerates to
|
||||
/// the plain line whenever the sweep collapses edge-on (the authored view).
|
||||
case poly(points: [CGPoint], width: Double, ink: PropInk)
|
||||
case circle(center: CGPoint, radius: Double, width: Double, fill: Bool, ink: PropInk)
|
||||
}
|
||||
|
||||
/// A pose resolved to drawable canvas points (plus the depth-sorted draw order and
|
||||
/// per-limb shading the view needs).
|
||||
struct FigureGeometry {
|
||||
@@ -171,6 +190,10 @@ struct FigureGeometry {
|
||||
/// Parts far-to-near, `"head"` always last (`"spine"`, `"arm_r"`, … then `"head"`).
|
||||
var order: [String]
|
||||
var shade: [FigureLimb: Shade]
|
||||
/// The equipment layer resolved for this frame: scene shapes and cables drawn
|
||||
/// behind the figure, joint-attached items drawn over the limbs (before the head).
|
||||
var propsBackground: [PropPrimitive] = []
|
||||
var propsForeground: [PropPrimitive] = []
|
||||
}
|
||||
|
||||
/// FK output in view space (x right, y up, z toward the camera; origin at the root).
|
||||
@@ -500,6 +523,160 @@ enum MotionSolver {
|
||||
floor: floor, limbs: limbs, order: order, shade: shade)
|
||||
return (resolved, geo)
|
||||
}
|
||||
|
||||
// MARK: Props
|
||||
|
||||
/// Prop joint refs → (limb, chain index): extremities are index 2, mid joints
|
||||
/// (elbows/knees) index 1, so equipment can ride either joint. Kept 1:1 with
|
||||
/// the reference renderer's `JOINT_LIMB` (legs end in a foot bone, so a foot
|
||||
/// prop rides the ankle at index 2).
|
||||
private static let propJoints: [String: (limb: FigureLimb, index: Int)] = [
|
||||
"hand_r": (.armR, 2), "elbow_r": (.armR, 1),
|
||||
"hand_l": (.armL, 2), "elbow_l": (.armL, 1),
|
||||
"foot_r": (.legR, 2), "knee_r": (.legR, 1),
|
||||
"foot_l": (.legL, 2), "knee_l": (.legL, 1),
|
||||
]
|
||||
|
||||
/// The joint point a prop follows (plus the unit direction of the bone ending
|
||||
/// at the first joint, for perpendicular items). Nil when any referenced limb
|
||||
/// isn't drawn this frame.
|
||||
private static func jointAnchor(_ geo: FigureGeometry, _ ref: PropJointRef) -> (point: CGPoint, direction: CGVector)? {
|
||||
var points: [CGPoint] = []
|
||||
var direction: CGVector?
|
||||
for name in ref.names {
|
||||
guard let joint = propJoints[name],
|
||||
let chain = geo.limbs[joint.limb], chain.count > joint.index else { return nil }
|
||||
let a = chain[joint.index - 1]
|
||||
let b = chain[joint.index]
|
||||
points.append(b)
|
||||
if direction == nil {
|
||||
let d = max(hypot(b.x - a.x, b.y - a.y), 1)
|
||||
direction = CGVector(dx: (b.x - a.x) / d, dy: (b.y - a.y) / d)
|
||||
}
|
||||
}
|
||||
guard let direction, !points.isEmpty else { return nil }
|
||||
let center = CGPoint(x: points.reduce(0) { $0 + $1.x } / CGFloat(points.count),
|
||||
y: points.reduce(0) { $0 + $1.y } / CGFloat(points.count))
|
||||
return (center, direction)
|
||||
}
|
||||
|
||||
/// View-space rotation carrying authored-view prop geometry to the camera
|
||||
/// `yawOffset` degrees past the authored yaw: a rotation about the world-vertical
|
||||
/// axis through the root anchor, conjugated by the camera elevation. Nil (the
|
||||
/// identity) at offset 0, so the authored view stays bit-exact.
|
||||
static func propRotation(pitch: Double, yawOffset: Double) -> Mat3? {
|
||||
guard yawOffset != 0 else { return nil }
|
||||
return Mat3.rotX(pitch).times(Mat3.rotY(-yawOffset)).times(Mat3.rotX(-pitch))
|
||||
}
|
||||
|
||||
/// Props → drawable primitives for one frame: (background, foreground).
|
||||
///
|
||||
/// `geo` is the frame's drawn (possibly orbit-rotated) geometry, `authored` the
|
||||
/// same frame at the authored camera (pass `geo` itself when not orbiting),
|
||||
/// `anchor` the frame's root canvas anchor, and `rotation` the `propRotation`
|
||||
/// between them. Joint positions come from `geo`; everything authored — scene
|
||||
/// points, cable anchors, bar angles, pad perpendiculars, roller offsets —
|
||||
/// resolves against `authored` and rotates. Kept 1:1 with the reference
|
||||
/// renderer's `resolve_props` — change them in lockstep.
|
||||
static func resolveProps(_ props: [MotionProp], geo: FigureGeometry, authored: FigureGeometry,
|
||||
anchor: CGPoint, rotation: Mat3?) -> (bg: [PropPrimitive], fg: [PropPrimitive]) {
|
||||
/// Authored canvas point (+ depth toward the camera) → drawn canvas.
|
||||
func place(_ x: Double, _ y: Double, _ z: Double) -> CGPoint {
|
||||
guard let rotation else { return CGPoint(x: x, y: y) }
|
||||
let v = rotation.apply(Vec3(x - anchor.x, anchor.y - y, z))
|
||||
return CGPoint(x: anchor.x + v.x, y: anchor.y - v.y)
|
||||
}
|
||||
/// Authored canvas-space direction/offset → drawn canvas (y-down).
|
||||
func swing(_ vec: CGVector) -> CGVector {
|
||||
guard let rotation else { return vec }
|
||||
let v = rotation.apply(Vec3(vec.dx, -vec.dy, 0))
|
||||
return CGVector(dx: v.x, dy: -v.y)
|
||||
}
|
||||
|
||||
var bg: [PropPrimitive] = []
|
||||
var fg: [PropPrimitive] = []
|
||||
for prop in props {
|
||||
switch prop.type {
|
||||
case "scene":
|
||||
for shape in prop.shapes ?? [] {
|
||||
let ink = PropInk(shape.color)
|
||||
let z = shape.z ?? 0
|
||||
switch shape.kind {
|
||||
case "line":
|
||||
guard let pts = shape.pts else { continue }
|
||||
let lifted = pts.compactMap { pt -> (x: Double, y: Double, z: Double)? in
|
||||
pt.count >= 2 ? (pt[0], pt[1], z + (pt.count > 2 ? pt[2] : 0)) : nil
|
||||
}
|
||||
if let depth = shape.depth, depth != 0 {
|
||||
// An extruded slab: the polyline swept through ±depth,
|
||||
// filled and outlined so it degenerates to the plain
|
||||
// line whenever the sweep collapses edge-on.
|
||||
let quad = lifted.map { place($0.x, $0.y, $0.z + depth) }
|
||||
+ lifted.reversed().map { place($0.x, $0.y, $0.z - depth) }
|
||||
bg.append(.poly(points: quad, width: shape.w ?? 4, ink: ink))
|
||||
} else {
|
||||
bg.append(.line(points: lifted.map { place($0.x, $0.y, $0.z) },
|
||||
width: shape.w ?? 4, ink: ink))
|
||||
}
|
||||
case "circle":
|
||||
guard let c = shape.c, c.count == 2, let r = shape.r else { continue }
|
||||
bg.append(.circle(center: place(c[0], c[1], z), radius: r,
|
||||
width: shape.w ?? 3, fill: shape.fill ?? false, ink: ink))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
case "cable":
|
||||
guard let from = prop.from, from.count >= 2, let to = prop.to,
|
||||
let end = jointAnchor(geo, to) else { continue }
|
||||
let start = place(from[0], from[1], from.count > 2 ? from[2] : 0)
|
||||
bg.append(.line(points: [start, end.point], width: prop.w ?? 2, ink: .equipment))
|
||||
case "roller":
|
||||
// A machine roller pad seen end-on: a disc riding the limb's lower
|
||||
// bone near the joint, on the `side` (+1/−1) of the bone it presses.
|
||||
// The offset resolves along the authored-view bone, then rotates.
|
||||
guard let at = prop.at, let now = jointAnchor(geo, at),
|
||||
let auth = jointAnchor(authored, at) else { continue }
|
||||
let r = prop.r ?? 5
|
||||
let back = prop.back ?? 0
|
||||
let side = prop.side ?? 1
|
||||
let d = auth.direction
|
||||
let px = d.dy * side, py = -d.dx * side
|
||||
let off = swing(CGVector(dx: -d.dx * back + px * (r + 3),
|
||||
dy: -d.dy * back + py * (r + 3)))
|
||||
fg.append(.circle(center: CGPoint(x: now.point.x + off.dx, y: now.point.y + off.dy),
|
||||
radius: r, width: 3, fill: true, ink: .prop))
|
||||
case "bar", "dumbbell", "pad":
|
||||
let defaults: (halfLen: Double, width: Double, plateR: Double) = switch prop.type {
|
||||
case "bar": (24, 4, 0)
|
||||
case "dumbbell": (7, 3, 4.5)
|
||||
default: (8, 7, 0)
|
||||
}
|
||||
guard let at = prop.at, let now = jointAnchor(geo, at),
|
||||
let auth = jointAnchor(authored, at) else { continue }
|
||||
let axis: CGVector
|
||||
if prop.type == "bar" || prop.angle != nil {
|
||||
axis = direction(prop.angle ?? 0) // fixed authored-view angle
|
||||
} else {
|
||||
axis = CGVector(dx: -auth.direction.dy, dy: auth.direction.dx)
|
||||
}
|
||||
let u = swing(axis) // foreshortens under orbit
|
||||
let h = prop.halfLen ?? defaults.halfLen
|
||||
let a = CGPoint(x: now.point.x - u.dx * h, y: now.point.y - u.dy * h)
|
||||
let b = CGPoint(x: now.point.x + u.dx * h, y: now.point.y + u.dy * h)
|
||||
fg.append(.line(points: [a, b], width: prop.w ?? defaults.width, ink: .prop))
|
||||
let plateR = prop.plateR ?? defaults.plateR
|
||||
if plateR > 0 {
|
||||
for end in [a, b] {
|
||||
fg.append(.circle(center: end, radius: plateR, width: 3, fill: true, ink: .prop))
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return (bg, fg)
|
||||
}
|
||||
}
|
||||
|
||||
/// The full looping animation for one motion: key frames resolved to anatomical angles,
|
||||
@@ -566,13 +743,28 @@ struct MotionTimeline {
|
||||
/// targets in the *authored* view, so the pose resolves there first and the posed
|
||||
/// body is then rotated; re-pinning in a rotated view would glue hands to screen
|
||||
/// points that no longer correspond to anything (arms visibly "stuck" mid-orbit).
|
||||
func geometry(at time: Double, yawOffset: Double = 0) -> FigureGeometry {
|
||||
/// `props` are resolved into the geometry's primitive layers the same way: joint
|
||||
/// anchors follow the rotated figure, while authored constructs (scene points,
|
||||
/// cable anchors, bar angles, pad perpendiculars) resolve in the authored view
|
||||
/// and rotate about the root anchor's vertical axis.
|
||||
func geometry(at time: Double, yawOffset: Double = 0, props: [MotionProp] = []) -> FigureGeometry {
|
||||
let frame = frame(at: time)
|
||||
guard yawOffset != 0 else {
|
||||
return MotionSolver.frameGeometry(frame, prof: profile, cam: cam, pitch: pitch, mat: mat).1
|
||||
var geo: FigureGeometry
|
||||
let authored: FigureGeometry
|
||||
if yawOffset == 0 {
|
||||
geo = MotionSolver.frameGeometry(frame, prof: profile, cam: cam, pitch: pitch, mat: mat).1
|
||||
authored = geo
|
||||
} else {
|
||||
let (posedFrame, authoredGeo) = MotionSolver.frameGeometry(frame, prof: profile, cam: cam, pitch: pitch)
|
||||
authored = authoredGeo
|
||||
var posed = posedFrame
|
||||
posed.pins = [:]
|
||||
geo = MotionSolver.frameGeometry(posed, prof: profile, cam: cam + yawOffset, pitch: pitch, mat: mat).1
|
||||
}
|
||||
var posed = MotionSolver.frameGeometry(frame, prof: profile, cam: cam, pitch: pitch).0
|
||||
posed.pins = [:]
|
||||
return MotionSolver.frameGeometry(posed, prof: profile, cam: cam + yawOffset, pitch: pitch, mat: mat).1
|
||||
guard !props.isEmpty else { return geo }
|
||||
let rotation = MotionSolver.propRotation(pitch: pitch, yawOffset: yawOffset)
|
||||
(geo.propsBackground, geo.propsForeground) = MotionSolver.resolveProps(
|
||||
props, geo: geo, authored: authored, anchor: frame.rootPos, rotation: rotation)
|
||||
return geo
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user