110 lines
3.4 KiB
PHP
110 lines
3.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* A 3MF `transform` attribute is 12 space-separated finite floats describing
|
|
* an affine map as three row basis vectors plus a translation: [Xx Xy Xz Yx
|
|
* Yy Yz Zx Zy Zz Tx Ty Tz]. A point is mapped as
|
|
* p' = p.x*X + p.y*Y + p.z*Z + T (row-vector convention). Composing two
|
|
* transforms (e.g. a build item transform applied on top of a component's
|
|
* own transform) must yield the same result as applying the inner transform
|
|
* first and then the outer one to the resulting point.
|
|
*
|
|
* @phpstan-type Matrix array{0:float,1:float,2:float,3:float,4:float,5:float,6:float,7:float,8:float,9:float,10:float,11:float}
|
|
*/
|
|
class ThreeMfTransform
|
|
{
|
|
/**
|
|
* @return Matrix
|
|
*/
|
|
public static function identity(): array
|
|
{
|
|
return [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0];
|
|
}
|
|
|
|
/**
|
|
* @return Matrix
|
|
*/
|
|
public static function parse(string $raw): array
|
|
{
|
|
$parts = preg_split('/\s+/', trim($raw)) ?: [];
|
|
if (count($parts) !== 12) {
|
|
throw new RuntimeException('3MF transform must have exactly 12 values.');
|
|
}
|
|
|
|
$values = [];
|
|
foreach ($parts as $part) {
|
|
if (! is_numeric($part)) {
|
|
throw new RuntimeException('3MF transform contains a non-numeric value.');
|
|
}
|
|
$value = (float) $part;
|
|
if (! is_finite($value)) {
|
|
throw new RuntimeException('3MF transform contains a non-finite value.');
|
|
}
|
|
$values[] = $value;
|
|
}
|
|
|
|
/** @var Matrix $values */
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Composes transforms so that apply(compose($outer, $inner), $p) ===
|
|
* apply($outer, apply($inner, $p)) for every point $p.
|
|
*
|
|
* @param Matrix $outer
|
|
* @param Matrix $inner
|
|
* @return Matrix
|
|
*/
|
|
public static function compose(array $outer, array $inner): array
|
|
{
|
|
$innerX = [$inner[0], $inner[1], $inner[2]];
|
|
$innerY = [$inner[3], $inner[4], $inner[5]];
|
|
$innerZ = [$inner[6], $inner[7], $inner[8]];
|
|
$innerT = [$inner[9], $inner[10], $inner[11]];
|
|
|
|
$newX = self::applyLinear($outer, $innerX);
|
|
$newY = self::applyLinear($outer, $innerY);
|
|
$newZ = self::applyLinear($outer, $innerZ);
|
|
$newT = self::apply($outer, $innerT);
|
|
|
|
return [
|
|
$newX[0], $newX[1], $newX[2],
|
|
$newY[0], $newY[1], $newY[2],
|
|
$newZ[0], $newZ[1], $newZ[2],
|
|
$newT[0], $newT[1], $newT[2],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param Matrix $m
|
|
* @param array{0:float,1:float,2:float} $p
|
|
* @return array{0:float,1:float,2:float}
|
|
*/
|
|
public static function apply(array $m, array $p): array
|
|
{
|
|
$linear = self::applyLinear($m, $p);
|
|
|
|
return [$linear[0] + $m[9], $linear[1] + $m[10], $linear[2] + $m[11]];
|
|
}
|
|
|
|
/**
|
|
* Applies only the linear (basis) part of the transform, ignoring translation.
|
|
*
|
|
* @param Matrix $m
|
|
* @param array{0:float,1:float,2:float} $p
|
|
* @return array{0:float,1:float,2:float}
|
|
*/
|
|
private static function applyLinear(array $m, array $p): array
|
|
{
|
|
return [
|
|
$p[0] * $m[0] + $p[1] * $m[3] + $p[2] * $m[6],
|
|
$p[0] * $m[1] + $p[1] * $m[4] + $p[2] * $m[7],
|
|
$p[0] * $m[2] + $p[1] * $m[5] + $p[2] * $m[8],
|
|
];
|
|
}
|
|
}
|