Initial commit

This commit is contained in:
Dominik Chrastecky 2022-11-08 20:32:20 +01:00
commit a82d15f939
101 changed files with 3561 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/.idea
/vendor
/composer.lock
/test.php
/.php-cs-fixer.cache

122
.php-cs-fixer.dist.php Normal file
View File

@ -0,0 +1,122 @@
<?php
use PhpCsFixer\Fixer\DoctrineAnnotation\DoctrineAnnotationBracesFixer;
$options = (new DoctrineAnnotationBracesFixer())
->getConfigurationDefinition()
->getOptions();
$ignoredTags = null;
foreach ($options as $option) {
if($option->getName() === 'ignored_tags') {
$ignoredTags = $option->getDefault();
break;
}
}
if ($ignoredTags === null) {
throw new RuntimeException('Could not get list of default rules');
}
$ignoredTags[] = 'Annotation';
$ignoredTags[] = 'extends';
$ignoredTags[] = 'template';
$ignoredTags[] = 'implements';
return (new PhpCsFixer\Config())
->setRules([
'@PSR2' => true,
'array_syntax' => ['syntax' => 'short'],
'blank_line_after_opening_tag' => true,
'blank_line_before_statement' => [
'statements' => ['declare', 'return', 'try'],
],
'cast_spaces' => true,
'class_attributes_separation' => true,
'combine_consecutive_unsets' => true,
'compact_nullable_typehint' => true,
'concat_space' => [
'spacing' => 'one',
],
'doctrine_annotation_braces' => [
'syntax' => 'with_braces',
'ignored_tags' => $ignoredTags,
],
'doctrine_annotation_spaces' => [
'ignored_tags' => $ignoredTags,
],
'doctrine_annotation_indentation' => [
'ignored_tags' => $ignoredTags
],
'explicit_indirect_variable' => true,
'explicit_string_variable' => true,
'final_class' => true,
'fully_qualified_strict_types' => true,
'function_typehint_space' => true,
'include' => true,
'linebreak_after_opening_tag' => true,
'lowercase_cast' => true,
'lowercase_static_reference' => true,
'magic_constant_casing' => true,
'magic_method_casing' => true,
'multiline_comment_opening_closing' => true,
'native_function_casing' => true,
'no_alternative_syntax' => true,
'no_blank_lines_after_class_opening' => true,
'no_blank_lines_after_phpdoc' => true,
'no_empty_comment' => true,
'no_empty_phpdoc' => true,
'no_empty_statement' => true,
'no_extra_blank_lines' => [
'tokens' => [
'extra',
'break',
'continue',
'curly_brace_block',
'parenthesis_brace_block',
'return',
'square_brace_block',
'throw',
'use',
'use_trait',
'switch',
'case',
'default',
],
],
'no_leading_import_slash' => true,
'no_leading_namespace_whitespace' => true,
'no_mixed_echo_print' => true,
'no_spaces_around_offset' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_unneeded_final_method' => true,
'no_unused_imports' => true,
'no_whitespace_before_comma_in_array' => true,
'no_whitespace_in_blank_line' => true,
'normalize_index_brace' => true,
'object_operator_without_whitespace' => true,
'ordered_class_elements' => true,
'ordered_imports' => true,
'protected_to_private' => true,
'short_scalar_cast' => true,
'single_blank_line_before_namespace' => true,
'single_line_comment_style' => true,
'single_quote' => true,
'standardize_not_equals' => true,
'ternary_operator_spaces' => true,
'trailing_comma_in_multiline' => [
'elements' => ['arrays', 'arguments', 'parameters'],
],
'unary_operator_spaces' => true,
'visibility_required' => [
'elements' => ['property', 'method', 'const'],
],
'whitespace_after_comma_in_array' => true,
'phpdoc_align' => true,
'phpdoc_indent' => true,
'phpdoc_no_package' => true,
'phpdoc_order' => true,
'phpdoc_scalar' => true,
'phpdoc_separation' => true,
]);

498
README.md Normal file
View File

@ -0,0 +1,498 @@
# PhpScad - 3D modelling using OpenSCAD in PHP
Ever wanted to create a 3D model in php? Sure you did, *everyone* does. And now you can.
## How does it work?
It generates OpenSCAD code that in turn creates the STL 3D model. OpenScad basically looks like this:
```openscad
translate([10, 0, 0]) // translate([x, y, z]) - moves the model by given coordinates
cube([10, 10, 10]); // cube([width, depth, height])
difference() { // only the difference between child objects is rendered
cube([5, 10, 15]);
cylinder(10, 5, 5); // cylinder(height, bottomRadius, topRadius)
}
```
This is the resulting 3D model preview:
![OpenSCAD example](/doc/img/example-openscad.png)
### So why use php if you can already create the model using code?
1. PHP is full-blown general-purpose language with a lot of documentation around the internet
2. There are full-blown IDEs that help with code completion
3. PHP supports more paradigms, like object oriented programming
4. Saner parameter names - for example the `cylinder` signature is `cylinder(h, r, r1, r2, d, d1, d2, center)`
compared to PhpScad version - `new Cylinder(height, radius, bottomRadius, topRadius, diameter, bottomDiameter, topDiameter)`
- note that in both OpenSCAD and PhpScad version many of the parameters are optional
5. Created an interesting parametric shape? Cool, share it via composer because PHP has a package manager!
### So how does the example above look in PHP?
```php
<?php
use Rikudou\PhpScad\ScadModel;
use Rikudou\PhpScad\Shape\Cube;
use Rikudou\PhpScad\Combination\Difference;
use Rikudou\PhpScad\Shape\Cylinder;
$model = new ScadModel();
$model = $model
->withRenderable((new Cube(width: 10, depth: 10, height: 10))->movedRight(10)) // using named parameters
->withRenderable(new Difference(
new Cube(5, 10, 15), // using positional parameters
new Cylinder(height: 10, bottomRadius: 5, topRadius: 5),
));
$model->render(__DIR__ . '/output.scad');
```
You can notice the preview looks the same:
![Same example as above but in php](/doc/img/example-php-openscad.png)
Notice the convenience method `->movedRight()` which is one of the examples of what's possible in PHP but not in
OpenSCAD - a fluent api that's more natural and easy to think about.
You can also go the OpenSCAD way in PhpScad:
```php
<?php
use Rikudou\PhpScad\Transformation\Translate;
use Rikudou\PhpScad\Coordinate\XYZ;
use Rikudou\PhpScad\Shape\Cube;
new Translate(new XYZ(x: 10, y: 0, z: 0), new Cube(width: 10, depth: 10, height: 10));
```
![Manual translate](/doc/img/example-manual-translate.png)
## Installation
`composer require rikudou/php-scad:dev-master`
## Usage
### Shapes
Shapes are the base of all models. PhpScad provides the same basic shades as OpenSCAD does, namely:
- Cube
- Cylinder
- Polyhedron
- Sphere
Additional shapes provided by PhpScad:
- Pyramid
Basically all shapes can be created by combining the basic shapes.
#### Cube
Creates a cube.
**Parameters**
- `number $width` (default: 0)
- `number $depth` (default: 0)
- `number $height` (default: 0)
**Rendered if**: At least one of the parameters is non-zero or is a reference type.
**Example**:
```php
<?php
use Rikudou\PhpScad\Shape\Cube;
$cube = new Cube(width: 10, depth: 10, height: 10);
```
![Cube](/doc/img/example-cube.png)
#### Cylinder
Creates a cylinder.
**Parameters**
- `number $height` (default: 0)
- `?number $radius` (default: null)
- `?number $bottomRadius` (default: null)
- `?number $topRadius` (default: null)
- `?number $diameter` (default: null)
- `?number $bottomDiameter` (default: null)
- `?number $topDiameter` (default: null)
- `bool $centerOnZ` (default: false) - whether the model should be centered on the Z axis
- `bool $centerOnXY` (default: true) - whether the model should be centered on the X and Y axis
- `?FacetsConfiguration $facetsConfiguration` (default: null) - facets configuration, more below
You should either provide radius or diameter, not both. Also, you should provide either the singular parameter
(`$radius`, `$diameter`) or one or both of the top/bottom pair
(`$bottomRadius`/`$topRadius`, `$bottomDiameter`/`$topDiameter).
Using a pair of radii/diameters allows you to create a cone.
If you provide invalid combination of radii, the behavior is undefined and depends on the OpenSCAD implementation as
PhpScad will generate the shape with all the parameters you provide.
**Rendered if**: Height is non-zero and at least one of the radius parameters is provided.
**Example**:
```php
<?php
use Rikudou\PhpScad\Shape\Cylinder;
// all these shapes are equivalent
$cylinderWithDiameter = new Cylinder(height: 10, diameter: 20);
$cylinderWithTopBottomDiameters = new Cylinder(height: 10, topDiameter: 20, bottomDiameter: 20);
$cylinderWithRadius = new Cylinder(height: 10, radius: 10);
$cylinderWithTopBottomRadii = new Cylinder(height: 10, topRadius: 10, bottomRadius: 10);
// cone
$cone = new Cylinder(height: 10, topRadius: 10, bottomRadius: 20);
// fully centered
$centered = new Cylinder(height: 10, diameter: 10, centerOnXY: true, centerOnZ: true);
```
![Basic cylinder](/doc/img/example-cylinder.png)
![Cylinder - cone](/doc/img/example-cylinder-cone.png)
![Cylinder - centered](/doc/img/example-cylinder-centered.png)
#### Polyhedron
The most versatile of shapes, allows you to define your own shapes using points and faces.
It can be used to create any regular or irregular shape.
**Parameters**
- `array|PointVector $points` (default: empty PointVector) - all points that the shape will consist of
- `array|FaceVector $faces` (default: empty FaceVector) - the faces the shape will consist of
- `int $convexity` (default: 1) - specifies the maximum number of faces a ray intersecting the object might penetrate, only used in preview mode
While arrays may be used for both points and faces, using the provided `PointVector` and `FaceVector` is recommended
for better readability. When using `FaceVector` you don't have to populate `PointVector` manually.
There are basically two ways to create a polyhedron, let's call them "the OpenSCAD way" and "the PhpScad way".
#### Polyhedron - the OpenSCAD way
> I can't think of any reason to use this instead of the other way, but it's supported for the sake of completeness, feel
> free to skip this part of the documentation.
First you need to define an array of points, you can either use `PointVector` or an array:
```php
<?php
use Rikudou\PhpScad\Value\PointVector;
use Rikudou\PhpScad\Value\Point;
// five points to make a pyramid, in no particular order
$points = [
[10, 15, 0],
[10, 0, 0],
[5, 7.5, 20],
[0, 0, 0],
[0, 15, 0],
];
// using PointVector
$points = new PointVector(
new Point(x: 10, y: 15, z: 0),
new Point(x: 10, y: 0, z: 0),
new Point(x: 5, y: 7.5, z: 20),
new Point(x: 0, y: 0, z: 0),
new Point(x: 0, y: 15, z: 0),
);
```
Then you need to reference those points when creating faces, using an index of the points in the points array:
> All faces must have their points ordered in **clockwise** direction when looking at each face from outside **inward**.
```php
<?php
use Rikudou\PhpScad\Shape\Polyhedron;
use Rikudou\PhpScad\Value\PointVector;
use Rikudou\PhpScad\Value\Point;
use Rikudou\PhpScad\Value\FaceVector;
use Rikudou\PhpScad\Value\Face;
// from previous example, added manual indexes for clarity
$points = [
0 => [10, 15, 0],
1 => [10, 0, 0],
2 => [5, 7.5, 20],
3 => [0, 0, 0],
4 => [0, 15, 0],
];
$faces = [
new Face(0, 1, 2), // points with indexes 0, 1 and 2, meaning '[10, 15, 0]', '[10, 0, 0]' and '[5, 7.5, 20]'
new Face(1, 3, 2), // points with indexes 1, 3 and 2, meaning '[10, 0, 0]', '[0, 0, 0]' and '[5, 7.5, 20]'
new Face(3, 4, 2), // '[0, 0, 0]', '[0, 15, 0]' and '[5, 7.5, 20]'
new Face(4, 0, 2), // '[0, 15, 0]', '[10, 15, 0]', '[5, 7.5, 20]'
new Face(0, 4, 3, 1), // '[10, 15, 0]', '[0, 15, 0]', '[0, 0, 0]', '[10, 0, 0]'
];
$polyhedron = new Polyhedron(points: $points, faces: $faces);
// or the same example using provided DTOs
$points = new PointVector(
new Point(x: 10, y: 15, z: 0),
new Point(x: 10, y: 0, z: 0),
new Point(x: 5, y: 7.5, z: 20),
new Point(x: 0, y: 0, z: 0),
new Point(x: 0, y: 15, z: 0),
);
$faces = new FaceVector(
new Face(0, 1, 2),
new Face(1, 3, 2),
new Face(3, 4, 2),
new Face(4, 0, 2),
new Face(0, 4, 3, 1),
);
$polyhedron = new Polyhedron(points: $points, faces: $faces);
// unless I made some mistake, $polyhedron should now hold a proper pyramid
```
![Polyhedron example](/doc/img/example-polyhedron.png)
As you can see this is hard to work with.
#### Polyhedron - the PhpScad way
Instead of the complicated stuff with referencing points by their indexes you can simply create the points when creating
faces, the points array will be defined internally:
```php
<?php
use Rikudou\PhpScad\Shape\Polyhedron;
use Rikudou\PhpScad\Value\Point;
use Rikudou\PhpScad\Value\Face;
use Rikudou\PhpScad\Value\FaceVector;
// this should be the same pyramid as above (unless I made a mistake)
$faces = new FaceVector(
new Face(
new Point(x: 10, y: 15, z: 0),
new Point(x: 10, y: 0, z: 0),
new Point(x: 5, y: 7.5, z: 20),
),
new Face(
new Point(x: 10, y: 0, z: 0),
new Point(x: 0, y: 0, z: 0),
new Point(x: 5, y: 7.5, z: 20),
),
new Face(
new Point(x: 0, y: 0, z: 0),
new Point(x: 0, y: 15, z: 0),
new Point(x: 5, y: 7.5, z: 20),
),
new Face(
new Point(x: 0, y: 15, z: 0),
new Point(x: 10, y: 15, z: 0),
new Point(x: 5, y: 7.5, z: 20),
),
new Face(
new Point(x: 10, y: 15, z: 0),
new Point(x: 0, y: 15, z: 0),
new Point(x: 0, y: 0, z: 0),
new Point(x: 10, y: 0, z: 0),
)
);
$polyhedron = new Polyhedron(faces: $faces);
// even more readable would be using good old variables like this:
$topPoint = new Point(x: 5, y: 7.5, z: 20);
$bottomLeft = new Point(x: 0, y: 0, z: 0);
$bottomRight = new Point(x: 10, y: 0, z: 0);
$topRight = new Point(x: 10, y: 15, z: 0);
$topLeft = new Point(x: 0, y: 15, z: 0);
// voila, perfectly readable
$polyhedron = new Polyhedron(faces: new FaceVector(
new Face($topRight, $bottomRight, $topPoint),
new Face($bottomRight, $bottomLeft, $topPoint),
new Face($bottomLeft, $topLeft, $topPoint),
new Face($topLeft, $topRight, $topPoint),
new Face($topRight, $topLeft, $bottomLeft, $bottomRight),
));
```
![Polyhedron example](/doc/img/example-polyhedron.png)
#### Sphere
Creates a sphere.
**Parameters**
- `?number $radius` (default: null)
- `?number $diameter` (default: null)
- `bool $center` (default: true) - whether to center the sphere on axis X, Y and Z
- `?FacetsConfiguration $facetsConfiguration` (default: null) - facets configuration, more below
If both radius and diameter is specified, radius takes precedence.
**Rendered if**: At radius or diameter is non-zero or is a reference type.
**Example**:
```php
<?php
use Rikudou\PhpScad\Shape\Sphere;
$sphere = new Sphere(radius: 5);
$sphere = new Sphere(diameter: 10);
$sphere = new Sphere(radius: 5, center: false);
```
![Sphere](/doc/img/example-sphere.png)
![Non-centered sphere](/doc/img/example-sphere-non-centered.png)
#### Pyramid
Creates a pyramid.
**Parameters**
- `number $width`
- `number $depth`
- `number $height`
**Rendered if**: Always
**Example**:
```php
<?php
use Rikudou\PhpScad\Shape\Pyramid;
$pyramid = new Pyramid(width: 10, depth: 10, height: 10);
```
![Pyramid](/doc/img/example-pyramid.png)
### Facets configuration
A STL model is made of triangles which are very much incompatible with shapes like cylinders and spheres, so you have
to cheat a little - you create a sphere of many, many triangles until you're ok with the way the sphere looks.
And that's where the facets configuration comes in - it configures how spherical things will look.
Signatures:
- `new FacetsNumber(float $numberOfFragments)`
- `new FacetsAngleAndSize(float $angle, float $size)`
- `$numberOfFragments` - the circle is rendered using this number of fragments
- `$angle` - minimum angle for a fragment, no circle has more than 360 divided by this number
- `$size` - minimum size of a fragment
**Examples:**
Default:
```php
<?php
use Rikudou\PhpScad\Shape\Sphere;
$sphere = new Sphere(radius: 10);
```
![Default facets](/doc/img/sphere-facets-default.png);
Facets number 60:
```php
<?php
use Rikudou\PhpScad\Shape\Sphere;
use Rikudou\PhpScad\FacetsConfiguration\FacetsNumber;
$sphere = new Sphere(radius: 10, facetsConfiguration: new FacetsNumber(60));
```
![60 facets](/doc/img/sphere-fn-60.png);
Facets number 120:
```php
<?php
use Rikudou\PhpScad\Shape\Sphere;
use Rikudou\PhpScad\FacetsConfiguration\FacetsNumber;
$sphere = new Sphere(radius: 10, facetsConfiguration: new FacetsNumber(120));
```
![120 facets](/doc/img/sphere-fn-120.png);
Facets number 240:
```php
<?php
use Rikudou\PhpScad\Shape\Sphere;
use Rikudou\PhpScad\FacetsConfiguration\FacetsNumber;
$sphere = new Sphere(radius: 10, facetsConfiguration: new FacetsNumber(240));
```
![120 facets](/doc/img/sphere-fn-240.png);
Facets number 360:
```php
<?php
use Rikudou\PhpScad\Shape\Sphere;
use Rikudou\PhpScad\FacetsConfiguration\FacetsNumber;
$sphere = new Sphere(radius: 10, facetsConfiguration: new FacetsNumber(360));
```
![120 facets](/doc/img/sphere-fn-360.png);
As you can see, the more facets you use, the smoother the spherical stuff looks, but it also takes a longer time to render and
the object is more complex.
> Note: The facets configuration can be also set globally for the whole model instead of per shape basis
```php
<?php
use Rikudou\PhpScad\ScadModel;
use Rikudou\PhpScad\FacetsConfiguration\FacetsNumber;
$model = new ScadModel(facetsConfiguration: new FacetsNumber(30));
// all renderables will now use the above facets number as default
```
**This documentation is work in progress**

28
composer.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "rikudou/php-scad",
"description": "A PHP wrapper around OpenSCAD",
"type": "library",
"license": "MIT",
"autoload": {
"psr-4": {
"Rikudou\\PhpScad\\": "src/"
}
},
"authors": [
{
"name": "Dominik Chrastecky",
"email": "dominik@chrastecky.cz"
}
],
"require": {
"php": "^8.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.13",
"phpstan/phpstan": "^1.9"
},
"scripts": {
"fixer": "php-cs-fixer fix src --verbose --allow-risky=yes",
"phpstan": "phpstan analyse --level=max src"
}
}

BIN
doc/img/example-cube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
doc/img/example-pyramid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
doc/img/example-sphere.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
doc/img/sphere-fn-120.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
doc/img/sphere-fn-240.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
doc/img/sphere-fn-360.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
doc/img/sphere-fn-60.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

2
phpstan.neon.dist Normal file
View File

@ -0,0 +1,2 @@
parameters:
inferPrivatePropertyTypeFromConstructor: true

View File

@ -0,0 +1,11 @@
<?php
namespace Rikudou\PhpScad\Color;
abstract class AbstractColor implements Color
{
public function __toString(): string
{
return $this->getScadRepresentation();
}
}

10
src/Color/Color.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace Rikudou\PhpScad\Color;
use Stringable;
interface Color extends Stringable
{
public function getScadRepresentation(): string;
}

24
src/Color/HexColor.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace Rikudou\PhpScad\Color;
use Rikudou\PhpScad\Implementation\ValueConverter;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
use Rikudou\PhpScad\Value\StringValue;
final class HexColor extends AbstractColor
{
use ValueConverter;
public function __construct(
private readonly StringValue|Reference|string $hex,
private readonly NumericValue|Reference|float $alpha = 1.0,
) {
}
public function getScadRepresentation(): string
{
return "c = {$this->convertToValue($this->hex)}, alpha = {$this->alpha}";
}
}

24
src/Color/NamedColor.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace Rikudou\PhpScad\Color;
use Rikudou\PhpScad\Implementation\ValueConverter;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
use Rikudou\PhpScad\Value\StringValue;
final class NamedColor extends AbstractColor
{
use ValueConverter;
public function __construct(
private readonly StringValue|Reference|string $colorName,
private readonly NumericValue|Reference|float $alpha = 1.0,
) {
}
public function getScadRepresentation(): string
{
return "c = {$this->convertToValue($this->colorName)}, alpha = {$this->alpha}";
}
}

36
src/Color/RGBColor.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace Rikudou\PhpScad\Color;
use Rikudou\PhpScad\Implementation\ValueConverter;
use Rikudou\PhpScad\Value\Expression;
use Rikudou\PhpScad\Value\FloatValue;
use Rikudou\PhpScad\Value\IntValue;
use Rikudou\PhpScad\Value\Reference;
final class RGBColor extends AbstractColor
{
use ValueConverter;
public function __construct(
private readonly IntValue|Reference|int $red,
private readonly IntValue|Reference|int $green,
private readonly IntValue|Reference|int $blue,
private readonly FloatValue|Reference|float $alpha = 1.0,
) {
}
public function getScadRepresentation(): string
{
$red = $this->convertToValue($this->red);
$green = $this->convertToValue($this->green);
$blue = $this->convertToValue($this->blue);
$alpha = $this->convertToValue($this->alpha);
$red = $red->hasLiteralValue() ? $red->getValue() / 255 : new Expression("{$red} / 255");
$green = $green->hasLiteralValue() ? $green->getValue() / 255 : new Expression("{$green} / 255");
$blue = $blue->hasLiteralValue() ? $blue->getValue() / 255 : new Expression("{$blue} / 255");
return "c = [{$red}, {$green}, {$blue}], alpha = {$alpha}";
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Rikudou\PhpScad\Combination;
use Rikudou\PhpScad\Implementation\RenderableImplementation;
use Rikudou\PhpScad\Implementation\WrapperModuleDefinitions;
use Rikudou\PhpScad\Implementation\WrapperRenderableImplementation;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Primitive\WrapperRenderable;
final class Difference implements WrapperRenderable
{
use RenderableImplementation;
use WrapperModuleDefinitions;
use WrapperRenderableImplementation;
public function __construct(
Renderable ...$renderable,
) {
$this->renderables = $renderable;
}
protected function doRender(): string
{
return "difference() {{$this->renderRenderables()}}";
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Rikudou\PhpScad\Combination;
use Rikudou\PhpScad\Implementation\RenderableImplementation;
use Rikudou\PhpScad\Implementation\WrapperModuleDefinitions;
use Rikudou\PhpScad\Implementation\WrapperRenderableImplementation;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Primitive\WrapperRenderable;
final class RenderableContainer implements WrapperRenderable
{
use RenderableImplementation;
use WrapperModuleDefinitions;
use WrapperRenderableImplementation;
public function __construct(
Renderable ...$renderable,
) {
$this->renderables = $renderable;
}
protected function doRender(): string
{
return $this->renderRenderables();
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Rikudou\PhpScad\Coordinate;
use Rikudou\PhpScad\Implementation\ValueConverter;
use Rikudou\PhpScad\Value\Expression;
use Rikudou\PhpScad\Value\FloatValue;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
abstract class AbstractCoordinate implements Coordinate
{
use ValueConverter;
public function add(Coordinate $coordinate): Coordinate
{
$currentX = $this->convertToValue($this->getX());
$currentY = $this->convertToValue($this->getY());
$currentZ = $this->convertToValue($this->getZ());
$newX = $this->convertToValue($coordinate->getX());
$newY = $this->convertToValue($coordinate->getY());
$newZ = $this->convertToValue($coordinate->getZ());
return new XYZ(
$this->addValues($currentX, $newX),
$this->addValues($currentY, $newY),
$this->addValues($currentZ, $newZ),
);
}
private function addValues(NumericValue|Reference $value1, NumericValue|Reference $value2): NumericValue|Reference
{
if ($value1->hasLiteralValue() && $value2->hasLiteralValue()) {
return new FloatValue($value1->getValue() + $value2->getValue());
}
return new Expression("{$value1} + {$value2}");
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Rikudou\PhpScad\Coordinate;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
interface Coordinate
{
public function getX(): Reference|NumericValue|float;
public function getY(): Reference|NumericValue|float;
public function getZ(): Reference|NumericValue|float;
public function add(Coordinate $coordinate): self;
}

29
src/Coordinate/X.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace Rikudou\PhpScad\Coordinate;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
final class X extends AbstractCoordinate
{
public function __construct(
private readonly Reference|NumericValue|float $x,
) {
}
public function getX(): Reference|NumericValue|float
{
return $this->x;
}
public function getY(): float
{
return 0;
}
public function getZ(): float
{
return 0;
}
}

30
src/Coordinate/XY.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace Rikudou\PhpScad\Coordinate;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
final class XY extends AbstractCoordinate
{
public function __construct(
private readonly Reference|NumericValue|float $x,
private readonly Reference|NumericValue|float $y,
) {
}
public function getX(): Reference|NumericValue|float
{
return $this->x;
}
public function getY(): Reference|NumericValue|float
{
return $this->y;
}
public function getZ(): float
{
return 0;
}
}

31
src/Coordinate/XYZ.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace Rikudou\PhpScad\Coordinate;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
final class XYZ extends AbstractCoordinate
{
public function __construct(
private readonly Reference|NumericValue|float $x,
private readonly Reference|NumericValue|float $y,
private readonly Reference|NumericValue|float $z,
) {
}
public function getX(): Reference|NumericValue|float
{
return $this->x;
}
public function getY(): Reference|NumericValue|float
{
return $this->y;
}
public function getZ(): Reference|NumericValue|float
{
return $this->z;
}
}

29
src/Coordinate/Y.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace Rikudou\PhpScad\Coordinate;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
final class Y extends AbstractCoordinate
{
public function __construct(
private readonly Reference|NumericValue|float $y,
) {
}
public function getX(): float
{
return 0;
}
public function getY(): Reference|NumericValue|float
{
return $this->y;
}
public function getZ(): float
{
return 0;
}
}

29
src/Coordinate/Z.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace Rikudou\PhpScad\Coordinate;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
final class Z extends AbstractCoordinate
{
public function __construct(
private readonly Reference|NumericValue|float $z,
) {
}
public function getX(): float
{
return 0;
}
public function getY(): float
{
return 0;
}
public function getZ(): Reference|NumericValue|float
{
return $this->z;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Rikudou\PhpScad\Coordinate;
final class ZeroCoordinate extends AbstractCoordinate
{
public function getX(): float
{
return 0;
}
public function getY(): float
{
return 0;
}
public function getZ(): float
{
return 0;
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace Rikudou\PhpScad\Customizer;
use Rikudou\PhpScad\Primitive\CustomizerVariable;
abstract class AbstractCustomizerVariable implements CustomizerVariable
{
public function __construct(
private readonly string $name,
private readonly string|int|float|bool|array|null $value,
private readonly ?string $description = null,
) {
}
public function __toString(): string
{
return $this->getScadRepresentation();
}
public function getName(): string
{
$name = $this->name;
if (!str_starts_with($name, '$')) {
$name = "\${$name}";
}
return $name;
}
public function getValue(): string|int|float|bool|array|null
{
return $this->value;
}
public function getScadRepresentation(): string
{
return $this->getRepresentation($this->getValue());
}
public function getDescription(): ?string
{
return $this->description;
}
private function getRepresentation(float|array|bool|int|string|null $value): string
{
if (is_array($value)) {
$result = '[';
$result .= implode(', ', array_map(function (float|array|bool|int|string|null $value) {
return $this->getRepresentation($value);
}, $value));
$result .= ']';
return $result;
}
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
if (is_string($value)) {
return "\"{$value}\"";
}
if ($value === null) {
return 'undef';
}
return (string) $value;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Rikudou\PhpScad\Customizer;
abstract class AbstractNumericCustomizerVariable extends AbstractCustomizerVariable
{
public function __construct(
string $name,
float|int $value,
?string $description = null,
) {
parent::__construct($name, $value, $description);
}
public function getValue(): int|float
{
return parent::getValue();
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Rikudou\PhpScad\Customizer;
final class BoolCustomizerVariable extends AbstractCustomizerVariable
{
public function __construct(string $name, bool $value, ?string $description = null)
{
parent::__construct($name, $value, $description);
}
public function getValue(): bool
{
return parent::getValue();
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Rikudou\PhpScad\Customizer;
final class FloatCustomizerVariable extends AbstractNumericCustomizerVariable
{
public function __construct(string $name, float $value, ?string $description = null)
{
parent::__construct($name, $value, $description);
}
public function getValue(): float
{
return parent::getValue();
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Rikudou\PhpScad\Customizer;
final class IntCustomizerVariable extends AbstractNumericCustomizerVariable
{
public function __construct(string $name, int $value, ?string $description = null)
{
parent::__construct($name, $value, $description);
}
public function getValue(): int
{
return parent::getValue();
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Rikudou\PhpScad\Customizer;
final class NullCustomizerVariable extends AbstractCustomizerVariable
{
public function __construct(string $name, ?string $description = null)
{
parent::__construct($name, null, $description);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Rikudou\PhpScad\Customizer;
final class NumericCustomizerVariable extends AbstractNumericCustomizerVariable
{
}

View File

@ -0,0 +1,27 @@
<?php
namespace Rikudou\PhpScad\FacetsConfiguration;
final class FacetsAngleAndSize implements FacetsConfiguration
{
public function __construct(
private readonly float $angle,
private readonly float $size,
) {
}
public function getMinimumFragmentAngle(): float
{
return $this->angle;
}
public function getMinimumFragmentSize(): float
{
return $this->size;
}
public function getNumberOfFragments(): ?float
{
return null;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Rikudou\PhpScad\FacetsConfiguration;
interface FacetsConfiguration
{
public function getMinimumFragmentAngle(): ?float;
public function getMinimumFragmentSize(): ?float;
public function getNumberOfFragments(): ?float;
}

View File

@ -0,0 +1,26 @@
<?php
namespace Rikudou\PhpScad\FacetsConfiguration;
final class FacetsNumber implements FacetsConfiguration
{
public function __construct(
private readonly float $numberOfFragments,
) {
}
public function getMinimumFragmentAngle(): ?float
{
return null;
}
public function getMinimumFragmentSize(): ?float
{
return null;
}
public function getNumberOfFragments(): float
{
return $this->numberOfFragments;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Rikudou\PhpScad\Implementation;
use Rikudou\PhpScad\Primitive\Renderable;
trait AliasShape
{
use RenderableImplementation;
use ConditionalRenderable;
use WrapperModuleDefinitions;
use GetWrappedRenderable;
abstract protected function getAliasedShape(): Renderable;
protected function isRenderable(): bool
{
return true;
}
protected function doRender(): string
{
return $this->getWrapped($this->getAliasedShape())->render();
}
protected function getRenderables(): iterable
{
yield $this->getAliasedShape();
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Rikudou\PhpScad\Implementation;
trait ConditionalRenderable
{
public function render(): string
{
if ($this->isRenderable()) {
return $this->doRender();
}
return '';
}
abstract protected function doRender(): string;
abstract protected function isRenderable(): bool;
}

View File

@ -0,0 +1,44 @@
<?php
namespace Rikudou\PhpScad\Implementation;
use Rikudou\PhpScad\FacetsConfiguration\FacetsConfiguration;
trait FacetsConfigImplementation
{
private ?FacetsConfiguration $facetsConfiguration;
public function getFacetsConfiguration(): ?FacetsConfiguration
{
return $this->facetsConfiguration;
}
public function withFacetsConfiguration(?FacetsConfiguration $configuration): self
{
$clone = clone $this;
$clone->facetsConfiguration = $configuration;
return $clone;
}
protected function getFacetsParameters(): string
{
$content = '';
if (($fa = $this->facetsConfiguration?->getMinimumFragmentAngle()) !== null) {
$content .= "\$fa = {$fa},";
}
if (($fs = $this->facetsConfiguration?->getMinimumFragmentSize()) !== null) {
$content .= "\$fs = {$fs},";
}
if (($fn = $this->facetsConfiguration?->getNumberOfFragments()) !== null) {
$content .= "\$fn = {$fn},";
}
if ($content) {
$content = substr($content, 0, -1);
}
return $content;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Rikudou\PhpScad\Implementation;
use Rikudou\PhpScad\Primitive\HasWrappers;
use Rikudou\PhpScad\Primitive\Renderable;
trait GetWrappedRenderable
{
private function getWrapped(Renderable $renderable): Renderable
{
if (!$renderable instanceof HasWrappers) {
return $renderable;
}
foreach ($renderable->getWrappers() as $config) {
$renderable = $this->getWrapped(new ($config->class)(...$config->getArguments($renderable)));
}
return $renderable;
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace Rikudou\PhpScad\Implementation;
use Error;
use Rikudou\PhpScad\Color\Color;
use Rikudou\PhpScad\Coordinate\Coordinate;
use Rikudou\PhpScad\Coordinate\X;
use Rikudou\PhpScad\Coordinate\Y;
use Rikudou\PhpScad\Coordinate\Z;
use Rikudou\PhpScad\Coordinate\ZeroCoordinate;
use Rikudou\PhpScad\Transformation\ColorChange;
use Rikudou\PhpScad\Transformation\Translate;
use Rikudou\PhpScad\Value\Expression;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
use Rikudou\PhpScad\Wrapper\WrapperConfiguration;
use Rikudou\PhpScad\Wrapper\WrapperValuePlaceholder;
trait RenderableImplementation
{
use Wither;
use ValueConverter;
private Coordinate $position;
private ?Color $color = null;
public function getPosition(): Coordinate
{
try {
return $this->position;
} catch (Error) {
return new ZeroCoordinate();
}
}
public function withPosition(Coordinate $position): self
{
return $this->with('position', $position);
}
public function movedBy(Coordinate $coordinate): self
{
return $this->withPosition(
$this->getPosition()->add($coordinate),
);
}
public function movedRight(NumericValue|Reference|float $millimeters): self
{
return $this->movedBy(new X($this->convertToValue($millimeters)));
}
public function movedLeft(NumericValue|Reference|float $millimeters): self
{
return $this->movedBy(new X(new Expression("-{$millimeters}")));
}
public function movedUp(NumericValue|Reference|float $millimeters): self
{
return $this->movedBy(new Y($this->convertToValue($millimeters)));
}
public function movedDown(NumericValue|Reference|float $millimeters): self
{
return $this->movedBy(new Y(new Expression("-{$millimeters}")));
}
public function movedUpOnZ(NumericValue|Reference|float $millimeters): self
{
return $this->movedBy(new Z($this->convertToValue($millimeters)));
}
public function movedDownOnZ(NumericValue|Reference|float $millimeters): self
{
return $this->movedBy(new Z(new Expression("-{$millimeters}")));
}
public function getWrappers(): iterable
{
yield new WrapperConfiguration(
Translate::class,
$this->getPosition(),
new WrapperValuePlaceholder(),
);
yield new WrapperConfiguration(
ColorChange::class,
$this->getColor(),
new WrapperValuePlaceholder(),
);
}
public function getColor(): ?Color
{
return $this->color;
}
public function withColor(?Color $color): self
{
return $this->with('color', $color);
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace Rikudou\PhpScad\Implementation;
use InvalidArgumentException;
use Rikudou\PhpScad\Value\BoolValue;
use Rikudou\PhpScad\Value\Face;
use Rikudou\PhpScad\Value\FaceVector;
use Rikudou\PhpScad\Value\FloatValue;
use Rikudou\PhpScad\Value\IntValue;
use Rikudou\PhpScad\Value\NullValue;
use Rikudou\PhpScad\Value\Point;
use Rikudou\PhpScad\Value\PointVector;
use Rikudou\PhpScad\Value\StringValue;
use Rikudou\PhpScad\Value\Value;
use Rikudou\PhpScad\Value\VectorValue;
trait ValueConverter
{
private function convertToValue(
Value|int|float|null|bool|array|string $value,
string $mapEmptyArrayTo = VectorValue::class,
): Value {
if ($value instanceof Value) {
return $value;
}
if (is_int($value)) {
return new IntValue($value);
}
if (is_float($value)) {
return new FloatValue($value);
}
if ($value === null) {
return new NullValue();
}
if (is_bool($value)) {
return new BoolValue($value);
}
if (is_string($value)) {
return new StringValue($value);
}
if ($this->isPointsVector($value, is_a($mapEmptyArrayTo, PointVector::class, true))) {
return PointVector::fromArray($value);
}
if ($this->isFaceVector($value, is_a($mapEmptyArrayTo, FaceVector::class, true))) {
return new FaceVector(...$value);
}
if (is_array($value)) {
return new VectorValue($value);
}
throw new InvalidArgumentException('Type not supported: ' . gettype($value));
}
private function isFaceVector(mixed $array, bool $considerEmptyArrayAsFaceVector): bool
{
if (!is_array($array)) {
return false;
}
if (!count($array)) {
return $considerEmptyArrayAsFaceVector;
}
if (!array_is_list($array)) {
return false;
}
foreach ($array as $item) {
if (!$item instanceof Face) {
return false;
}
}
return true;
}
private function isPointsVector(mixed $array, bool $considerEmptyArrayAsPointsVector): bool
{
if (!is_array($array)) {
return false;
}
if (!count($array)) {
return $considerEmptyArrayAsPointsVector;
}
if (!array_is_list($array)) {
return false;
}
$invalidPoints = array_reduce($array, function (int $carry, mixed $item) {
if ($item instanceof Point) {
return $carry;
}
if (!is_array($item) || !array_is_list($item) || count($item) !== 3) {
return $carry + 1;
}
foreach ($item as $value) {
if (!is_int($value) && !is_float($value)) {
return $carry + 1;
}
}
return $carry;
}, 0);
if ($invalidPoints) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Rikudou\PhpScad\Implementation;
trait Wither
{
protected function with(string $property, mixed $value): self
{
$clone = clone $this;
$clone->{$property} = $value;
return $clone;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Rikudou\PhpScad\Implementation;
use Rikudou\PhpScad\Primitive\HasModuleDefinitions;
use Rikudou\PhpScad\Primitive\Renderable;
trait WrapperModuleDefinitions
{
public function getModules(): iterable
{
foreach ($this->getRenderables() as $renderable) {
if ($renderable instanceof HasModuleDefinitions) {
yield from $renderable->getModules();
}
}
}
/**
* @return iterable<Renderable>
*/
abstract protected function getRenderables(): iterable;
}

View File

@ -0,0 +1,59 @@
<?php
namespace Rikudou\PhpScad\Implementation;
use Rikudou\PhpScad\Primitive\Renderable;
trait WrapperRenderableImplementation
{
use ConditionalRenderable;
use GetWrappedRenderable;
use Wither;
use ValueConverter;
/**
* @var array<Renderable>
*/
protected array $renderables = [];
public function withRenderable(Renderable $renderable): self
{
$renderables = $this->renderables;
$renderables[] = $renderable;
return $this->with('renderables', $renderables);
}
/**
* @return iterable<Renderable>
*/
protected function getRenderables(): iterable
{
return $this->renderables;
}
protected function isRenderable(): bool
{
return count($this->renderables) > 0;
}
private function renderRenderables(): string
{
$result = '';
foreach ($this->getRenderables() as $renderable) {
$result .= $this->getWrapped($renderable)->render();
}
return $result;
}
private function renderUnwrappedRenderables(): string
{
$result = '';
foreach ($this->getRenderables() as $renderable) {
$result .= $renderable->render();
}
return $result;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Rikudou\PhpScad\Module;
use Rikudou\PhpScad\Primitive\Module;
final class CustomizerEndModule implements Module
{
public function getName(): string
{
return '__Customizer_End';
}
public function getDefinition(): string
{
return "module {$this->getName()}() {}";
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Rikudou\PhpScad\Module;
use Rikudou\PhpScad\Primitive\Module;
final class NonCenterableCylinderModule implements Module
{
public function getName(): string
{
return 'PhpScad_NonCenterableCylinder';
}
public function getDefinition(): string
{
return "module {$this->getName()}(h = undef, r = undef, r1 = undef, r2 = undef, d = undef, d1 = undef, d2 = undef, center = false, centerXY = true) {
radii = [
r == undef ? 0 : r,
r1 == undef ? 0 : r1,
r2 == undef ? 0 : r2,
d == undef ? 0 : d / 2,
d1 == undef ? 0 : d1 / 2,
d2 == undef ? 0 : d2 / 2,
];
move = centerXY ? 0 : max(radii);
translate([move, move])
cylinder(h = h, r = r, r1 = r1, r2 = r2, d = d, d1 = d1, d2 = d2, center = center);
}";
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Rikudou\PhpScad\Module;
use Rikudou\PhpScad\Primitive\Module;
final class NonCenterableSphereModule implements Module
{
public function getName(): string
{
return 'PhpScad_NonCenterableSphere';
}
public function getDefinition(): string
{
return "module {$this->getName()}(r = undef, d = undef, center = true) {
move = center ? 0 : r != undef ? r : d != undef ? d / 2 : 0;
translate([move, move, move])
sphere(r = r, d = d);
}";
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Rikudou\PhpScad\Primitive;
use Stringable;
interface CustomizerVariable extends Stringable
{
public function getName(): string;
public function getValue(): string|int|float|bool|array|null;
public function getScadRepresentation(): string;
public function getDescription(): ?string;
}

View File

@ -0,0 +1,12 @@
<?php
namespace Rikudou\PhpScad\Primitive;
use Rikudou\PhpScad\FacetsConfiguration\FacetsConfiguration;
interface HasFacetsConfig
{
public function getFacetsConfiguration(): ?FacetsConfiguration;
public function withFacetsConfiguration(?FacetsConfiguration $configuration): self;
}

View File

@ -0,0 +1,11 @@
<?php
namespace Rikudou\PhpScad\Primitive;
interface HasModuleDefinitions
{
/**
* @return iterable<Module>
*/
public function getModules(): iterable;
}

View File

@ -0,0 +1,13 @@
<?php
namespace Rikudou\PhpScad\Primitive;
use Rikudou\PhpScad\Wrapper\WrapperConfiguration;
interface HasWrappers
{
/**
* @return iterable<WrapperConfiguration>
*/
public function getWrappers(): iterable;
}

10
src/Primitive/Module.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace Rikudou\PhpScad\Primitive;
interface Module
{
public function getName(): string;
public function getDefinition(): string;
}

View File

@ -0,0 +1,19 @@
<?php
namespace Rikudou\PhpScad\Primitive;
use Rikudou\PhpScad\Color\Color;
use Rikudou\PhpScad\Coordinate\Coordinate;
interface Renderable
{
public function getPosition(): Coordinate;
public function withPosition(Coordinate $coordinate): self;
public function getColor(): ?Color;
public function withColor(?Color $color): self;
public function render(): string;
}

View File

@ -0,0 +1,7 @@
<?php
namespace Rikudou\PhpScad\Primitive;
interface WrapperRenderable extends Renderable, HasModuleDefinitions, HasWrappers
{
}

View File

@ -0,0 +1,64 @@
<?php
namespace Rikudou\PhpScad\Renderer;
use RuntimeException;
abstract class AbstractOpenScadBinaryRenderer implements Renderer
{
protected const TARGET_FILE_PLACEHOLDER = '%targetFile%';
protected const SCAD_FILE_PLACEHOLDER = '%scadFile%';
protected string $binaryPath;
public function render(string $outputFile, string $scadContent): void
{
try {
$file = null;
$arguments = implode(' ', array_map(function (string $argument) use ($scadContent, $outputFile, &$file) {
if ($argument === self::TARGET_FILE_PLACEHOLDER) {
$argument = $outputFile;
}
if ($argument === self::SCAD_FILE_PLACEHOLDER) {
$renderer = new ScadFileRenderer();
$file = tempnam(sys_get_temp_dir(), 'PhpScad');
$renderer->render($file, $scadContent);
$argument = $file;
}
return "'{$argument}'";
}, $this->getArguments()));
$command = "'{$this->binaryPath}' {$arguments}";
exec("{$command} 2>&1 >/dev/null", result_code: $exitCode);
if ($exitCode !== 0) {
throw new RuntimeException("Failed to call system command: '{$command}'");
}
} finally {
if (is_string($file) && is_file($file)) {
unlink($file);
}
}
}
abstract protected function getArguments(): array;
protected function setBinary(string $binary): void
{
$this->binaryPath = $binary;
}
protected function findBinary(): string
{
exec('which openscad', $output, $exitCode);
if ($exitCode !== 0) {
throw new RuntimeException('Cannot find openscad in path');
}
return $output[0];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Rikudou\PhpScad\Renderer;
final class PngPreviewRenderer extends AbstractOpenScadBinaryRenderer
{
public function __construct(
private readonly ?int $width = null,
private readonly ?int $height = null,
?string $openScadPath = null,
) {
$this->setBinary($openScadPath ?? $this->findBinary());
}
protected function getArguments(): array
{
$args = [
'-o', self::TARGET_FILE_PLACEHOLDER,
'--export-format', 'png',
];
if ($this->width !== null && $this->height !== null) {
$args[] = '--imgsize';
$args[] = "{$this->width},{$this->height}";
}
$args[] = self::SCAD_FILE_PLACEHOLDER;
return $args;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Rikudou\PhpScad\Renderer;
interface Renderer
{
public function render(string $outputFile, string $scadContent): void;
}

View File

@ -0,0 +1,11 @@
<?php
namespace Rikudou\PhpScad\Renderer;
final class ScadFileRenderer implements Renderer
{
public function render(string $outputFile, string $scadContent): void
{
file_put_contents($outputFile, $scadContent);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Rikudou\PhpScad\Renderer;
final class StlRenderer extends AbstractOpenScadBinaryRenderer
{
public function __construct(
?string $openScadPath = null,
) {
$this->setBinary($openScadPath ?? $this->findBinary());
}
protected function getArguments(): array
{
return [
'--export-format', 'stl',
'-o', self::TARGET_FILE_PLACEHOLDER,
self::SCAD_FILE_PLACEHOLDER,
];
}
}

172
src/ScadModel.php Normal file
View File

@ -0,0 +1,172 @@
<?php
namespace Rikudou\PhpScad;
use JetBrains\PhpStorm\Immutable;
use Rikudou\PhpScad\FacetsConfiguration\FacetsConfiguration;
use Rikudou\PhpScad\Implementation\GetWrappedRenderable;
use Rikudou\PhpScad\Module\CustomizerEndModule;
use Rikudou\PhpScad\Primitive\CustomizerVariable;
use Rikudou\PhpScad\Primitive\HasModuleDefinitions;
use Rikudou\PhpScad\Primitive\Module;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Renderer\Renderer;
use Rikudou\PhpScad\Renderer\ScadFileRenderer;
#[Immutable]
final class ScadModel
{
use GetWrappedRenderable;
/**
* @var array<Module>
*/
private readonly array $modules;
/**
* @param array<Renderable> $renderables
* @param array<Module> $modules
* @param array<CustomizerVariable> $variables
*/
public function __construct(
private readonly ?FacetsConfiguration $facetsConfiguration = null,
private readonly Renderer $renderer = new ScadFileRenderer(),
private readonly array $renderables = [],
array $modules = [],
private readonly array $variables = [],
private readonly bool $configurableFacets = false,
) {
foreach ($modules as $key => $module) {
if ($key !== $module->getName()) {
unset($modules[$key]);
$modules[$module->getName()] = $module;
}
}
$this->modules = $modules;
}
public function withRenderable(Renderable $shape): self
{
$renderables = $this->renderables;
$renderables[] = $shape;
return new self(
facetsConfiguration: $this->facetsConfiguration,
renderer: $this->renderer,
renderables: $renderables,
modules: $this->modules,
variables: $this->variables,
configurableFacets: $this->configurableFacets,
);
}
public function withModule(Module $module): self
{
$modules = $this->modules;
$modules[$module->getName()] = $module;
return new self(
facetsConfiguration: $this->facetsConfiguration,
renderer: $this->renderer,
renderables: $this->renderables,
modules: $modules,
variables: $this->variables,
configurableFacets: $this->configurableFacets,
);
}
public function withFacetsConfiguration(FacetsConfiguration $configuration): self
{
return new self(
facetsConfiguration: $configuration,
renderer: $this->renderer,
renderables: $this->renderables,
modules: $this->modules,
variables: $this->variables,
configurableFacets: $this->configurableFacets,
);
}
public function withVariable(CustomizerVariable $variable): self
{
$variables = $this->variables;
$variables[] = $variable;
return new self(
facetsConfiguration: $this->facetsConfiguration,
renderer: $this->renderer,
renderables: $this->renderables,
modules: $this->modules,
variables: $variables,
configurableFacets: $this->configurableFacets,
);
}
public function render(string $outputFile): void
{
$this->renderer->render($outputFile, $this->getScadContent());
}
public function getScadContent(): string
{
$modules = $this->modules;
$customizerEndModule = new CustomizerEndModule();
if (array_key_first($modules) !== $customizerEndModule->getName()) {
array_unshift($modules, $customizerEndModule);
}
$variables = '';
if (count($this->variables)) {
foreach ($this->variables as $variable) {
if (($description = $variable->getDescription()) !== null) {
$variables .= "// {$description}\n";
}
$variables .= "{$variable->getName()} = {$variable->getScadRepresentation()};\n";
}
}
$content = '';
foreach ($this->renderables as $renderable) {
if ($renderable instanceof HasModuleDefinitions) {
$newModules = $renderable->getModules();
foreach ($newModules as $newModule) {
$modules[$newModule->getName()] = $newModule;
}
}
$content .= $this->getWrapped($renderable)->render();
}
$moduleContent = '';
foreach ($modules as $module) {
$moduleContent .= $module->getDefinition() . "\n";
}
$facets = '';
if ($fa = $this->facetsConfiguration?->getMinimumFragmentAngle()) {
$facets .= "\$fa = {$fa};\n";
}
if ($fs = $this->facetsConfiguration?->getMinimumFragmentSize()) {
$facets .= "\$fs = {$fs};\n";
}
if ($fn = $this->facetsConfiguration?->getNumberOfFragments()) {
$facets .= "\$fn = {$fn};\n";
}
$result = '';
if ($this->configurableFacets) {
$result .= $facets;
}
$result .= $variables;
$result .= $moduleContent;
if (!$this->configurableFacets) {
$result .= $facets;
}
$result .= $content;
return $result;
}
}

86
src/Shape/Cube.php Normal file
View File

@ -0,0 +1,86 @@
<?php
namespace Rikudou\PhpScad\Shape;
use JetBrains\PhpStorm\Immutable;
use Rikudou\PhpScad\Implementation\ConditionalRenderable;
use Rikudou\PhpScad\Implementation\RenderableImplementation;
use Rikudou\PhpScad\Implementation\ValueConverter;
use Rikudou\PhpScad\Implementation\Wither;
use Rikudou\PhpScad\Primitive\HasWrappers;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
#[Immutable(Immutable::PRIVATE_WRITE_SCOPE)]
final class Cube implements Renderable, HasWrappers
{
use RenderableImplementation;
use ConditionalRenderable;
use Wither;
use ValueConverter;
private NumericValue|Reference $width;
private NumericValue|Reference $depth;
private NumericValue|Reference $height;
public function __construct(
NumericValue|Reference|float $width = 0,
NumericValue|Reference|float $depth = 0,
NumericValue|Reference|float $height = 0,
) {
$this->width = $this->convertToValue($width);
$this->depth = $this->convertToValue($depth);
$this->height = $this->convertToValue($height);
}
public function getWidth(): NumericValue|Reference
{
return $this->width;
}
public function getDepth(): NumericValue|Reference
{
return $this->depth;
}
public function getHeight(): NumericValue|Reference
{
return $this->height;
}
public function withWidth(NumericValue|Reference|float $width): self
{
return $this->with('width', $this->convertToValue($width));
}
public function withDepth(NumericValue|Reference|float $depth): self
{
return $this->with('depth', $this->convertToValue($depth));
}
public function withHeight(NumericValue|Reference|float $height): self
{
return $this->with('height', $this->convertToValue($height));
}
protected function isRenderable(): bool
{
return (!$this->getDepth()->hasLiteralValue() || ($this->getDepth()->getValue() > 0))
|| (!$this->getWidth()->hasLiteralValue() || ($this->getWidth()->getValue() > 0))
|| (!$this->getHeight()->hasLiteralValue() || ($this->getHeight()->getValue() > 0))
;
}
protected function doRender(): string
{
return sprintf(
'cube([%s, %s, %s]);',
$this->width,
$this->depth,
$this->height,
);
}
}

227
src/Shape/Cylinder.php Normal file
View File

@ -0,0 +1,227 @@
<?php
namespace Rikudou\PhpScad\Shape;
use Rikudou\PhpScad\FacetsConfiguration\FacetsConfiguration;
use Rikudou\PhpScad\Implementation\ConditionalRenderable;
use Rikudou\PhpScad\Implementation\FacetsConfigImplementation;
use Rikudou\PhpScad\Implementation\RenderableImplementation;
use Rikudou\PhpScad\Implementation\ValueConverter;
use Rikudou\PhpScad\Implementation\Wither;
use Rikudou\PhpScad\Module\NonCenterableCylinderModule;
use Rikudou\PhpScad\Primitive\HasFacetsConfig;
use Rikudou\PhpScad\Primitive\HasModuleDefinitions;
use Rikudou\PhpScad\Primitive\HasWrappers;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Value\BoolValue;
use Rikudou\PhpScad\Value\NullValue;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
use Rikudou\PhpScad\Value\Value;
final class Cylinder implements Renderable, HasFacetsConfig, HasModuleDefinitions, HasWrappers
{
use RenderableImplementation;
use FacetsConfigImplementation;
use ConditionalRenderable;
use Wither;
use ValueConverter;
private NumericValue|Reference $height;
private NumericValue|Reference|NullValue $radius;
private NumericValue|Reference|NullValue $bottomRadius;
private NumericValue|Reference|NullValue $topRadius;
private NumericValue|Reference|NullValue $diameter;
private NumericValue|Reference|NullValue $bottomDiameter;
private NumericValue|Reference|NullValue $topDiameter;
private BoolValue|Reference $centerOnZ;
private BoolValue|Reference $centerOnXY;
public function __construct(
NumericValue|Reference|float $height = 0,
NumericValue|Reference|NullValue|float|null $radius = null,
NumericValue|Reference|NullValue|float|null $bottomRadius = null,
NumericValue|Reference|NullValue|float|null $topRadius = null,
NumericValue|Reference|NullValue|float|null $diameter = null,
NumericValue|Reference|NullValue|float|null $bottomDiameter = null,
NumericValue|Reference|NullValue|float|null $topDiameter = null,
BoolValue|Reference|bool $centerOnZ = false,
BoolValue|Reference|bool $centerOnXY = true,
?FacetsConfiguration $facetsConfiguration = null,
) {
$this->facetsConfiguration = $facetsConfiguration;
$this->height = $this->convertToValue($height);
$this->radius = $this->convertToValue($radius);
$this->bottomRadius = $this->convertToValue($bottomRadius);
$this->topRadius = $this->convertToValue($topRadius);
$this->diameter = $this->convertToValue($diameter);
$this->bottomDiameter = $this->convertToValue($bottomDiameter);
$this->topDiameter = $this->convertToValue($topDiameter);
$this->centerOnZ = $this->convertToValue($centerOnZ);
$this->centerOnXY = $this->convertToValue($centerOnXY);
}
public function withHeight(NumericValue|Reference|float $height): self
{
return $this->with('height', $this->convertToValue($height));
}
public function withRadius(NumericValue|Reference|NullValue|float|null $radius): self
{
return $this->with('radius', $this->convertToValue($radius));
}
public function withBottomRadius(NumericValue|Reference|NullValue|float|null $bottomRadius): self
{
return $this->with('bottomRadius', $this->convertToValue($bottomRadius));
}
public function withTopRadius(NumericValue|Reference|NullValue|float|null $topRadius): self
{
return $this->with('topRadius', $this->convertToValue($topRadius));
}
public function withDiameter(NumericValue|Reference|NullValue|float|null $diameter): self
{
return $this->with('diameter', $this->convertToValue($diameter));
}
public function withBottomDiameter(NumericValue|Reference|NullValue|float|null $bottomDiameter): self
{
return $this->with('bottomDiameter', $this->convertToValue($bottomDiameter));
}
public function withTopDiameter(NumericValue|Reference|NullValue|float|null $topDiameter): self
{
return $this->with('topDiameter', $this->convertToValue($topDiameter));
}
public function withCenterOnZ(BoolValue|bool $center): self
{
return $this->with('centerOnZ', $this->convertToValue($center));
}
public function withCenterOnXY(BoolValue|bool $center): self
{
return $this->with('centerOnXY', $this->convertToValue($center));
}
public function getModules(): iterable
{
yield new NonCenterableCylinderModule();
}
protected function doRender(): string
{
if (
!$this->radius instanceof NullValue
&& (
!$this->bottomRadius instanceof NullValue
|| !$this->topRadius instanceof NullValue
)
) {
trigger_error(
'You should not specify bottom radius or top radius if you specify radius',
E_USER_WARNING,
);
}
if (
!$this->diameter instanceof NullValue
&& (
!$this->topDiameter instanceof NullValue
|| !$this->bottomDiameter instanceof NullValue
)
) {
trigger_error(
'You should not specify bottom diameter or top diameter if you specify diameter',
E_USER_WARNING,
);
}
$matrix = [
[$this->radius, $this->bottomRadius, $this->topRadius],
[$this->diameter, $this->bottomDiameter, $this->topDiameter],
];
$unitsCount = array_reduce($matrix, function (int $carry, array $item) {
return $carry + (count(array_filter($item, fn (Value $value) => !$value instanceof NullValue)) ? 1 : 0);
}, 0);
if ($unitsCount > 1) {
trigger_error(
'You should not specify both radius and diameter',
E_USER_WARNING,
);
}
$content = 'PhpScad_NonCenterableCylinder(';
$content .= "h = {$this->height},";
$content .= sprintf('center = %s,', $this->centerOnZ);
$content .= sprintf('centerXY = %s,', $this->centerOnXY);
if (($radius = $this->radius) !== null) {
$content .= "r = {$radius},";
}
if (($radius = $this->bottomRadius) !== null) {
$content .= "r1 = {$radius},";
}
if (($radius = $this->topRadius) !== null) {
$content .= "r2 = {$radius},";
}
if (($diameter = $this->diameter) !== null) {
$content .= "d = {$diameter},";
}
if (($diameter = $this->topDiameter) !== null) {
$content .= "d2 = {$diameter},";
}
if (($diameter = $this->bottomDiameter) !== null) {
$content .= "d1 = {$diameter},";
}
$content = substr($content, 0, -1);
if ($facetsParameters = $this->getFacetsParameters()) {
$content .= ", {$facetsParameters}";
}
$content .= ');';
return $content;
}
protected function isRenderable(): bool
{
return !(
$this->height->hasLiteralValue()
&& $this->height->getValue() <= 0
&& $this->radius->hasLiteralValue()
&& ($this->radius->getValue() ?? 0) <= 0
&& $this->bottomRadius->hasLiteralValue()
&& ($this->bottomRadius->getValue() ?? 0) <= 0
&& $this->topRadius->hasLiteralValue()
&& ($this->topRadius->getValue() ?? 0) <= 0
&& $this->diameter->hasLiteralValue()
&& ($this->diameter->getValue() ?? 0) <= 0
&& $this->bottomDiameter->hasLiteralValue()
&& ($this->bottomDiameter->getValue() ?? 0) <= 0
&& $this->topDiameter->hasLiteralValue()
&& ($this->topDiameter->getValue() ?? 0) <= 0
);
}
}

62
src/Shape/Polyhedron.php Normal file
View File

@ -0,0 +1,62 @@
<?php
namespace Rikudou\PhpScad\Shape;
use Rikudou\PhpScad\Implementation\ConditionalRenderable;
use Rikudou\PhpScad\Implementation\RenderableImplementation;
use Rikudou\PhpScad\Implementation\ValueConverter;
use Rikudou\PhpScad\Implementation\Wither;
use Rikudou\PhpScad\Primitive\HasWrappers;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Value\Face;
use Rikudou\PhpScad\Value\FaceVector;
use Rikudou\PhpScad\Value\IntValue;
use Rikudou\PhpScad\Value\PointVector;
use Rikudou\PhpScad\Value\Reference;
final class Polyhedron implements Renderable, HasWrappers
{
use RenderableImplementation;
use ConditionalRenderable;
use ValueConverter;
use Wither;
private PointVector|Reference $points;
private FaceVector|Reference $faces;
private IntValue|Reference $convexity;
/**
* @param FaceVector|Reference|array<Face> $faces
*/
public function __construct(
PointVector|Reference|array $points = new PointVector(),
FaceVector|Reference|array $faces = new FaceVector(),
IntValue|Reference|int $convexity = 1,
) {
$this->points = $this->convertToValue($points, mapEmptyArrayTo: PointVector::class);
$this->faces = $this->convertToValue($faces, mapEmptyArrayTo: FaceVector::class);
$this->convexity = $this->convertToValue($convexity);
}
protected function doRender(): string
{
$points = clone $this->points;
$result = 'polyhedron(';
$result .= "faces = {$this->faces->getScadRepresentation($points)},";
$result .= "points = {$points},";
$result .= "convexity = {$this->convexity}";
$result .= ');';
return $result;
}
protected function isRenderable(): bool
{
return true;
}
}

63
src/Shape/Pyramid.php Normal file
View File

@ -0,0 +1,63 @@
<?php
namespace Rikudou\PhpScad\Shape;
use Rikudou\PhpScad\Implementation\AliasShape;
use Rikudou\PhpScad\Implementation\ValueConverter;
use Rikudou\PhpScad\Primitive\HasWrappers;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Value\Expression;
use Rikudou\PhpScad\Value\Face;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Point;
use Rikudou\PhpScad\Value\Reference;
final class Pyramid implements Renderable, HasWrappers
{
use AliasShape;
use ValueConverter;
private NumericValue|Reference $width;
private NumericValue|Reference $depth;
private NumericValue|Reference $height;
public function __construct(
NumericValue|Reference|float $width,
NumericValue|Reference|float $depth,
NumericValue|Reference|float $height,
) {
$this->width = $this->convertToValue($width);
$this->depth = $this->convertToValue($depth);
$this->height = $this->convertToValue($height);
}
protected function getAliasedShape(): Renderable
{
$topPoint = new Point(
x: $this->width->hasLiteralValue()
? $this->width->getValue() / 2
: new Expression("{$this->width} / 2"),
y: $this->depth->hasLiteralValue()
? $this->depth->getValue() / 2
: new Expression("{$this->depth} / 2"),
z: $this->height,
);
$bottomLeft = new Point(0, 0, 0);
$topLeft = new Point(0, $this->depth, 0);
$topRight = new Point($this->width, $this->depth, 0);
$bottomRight = new Point($this->width, 0, 0);
return $this->getWrapped(new Polyhedron(
faces: [
new Face($topRight, $bottomRight, $topPoint),
new Face($bottomRight, $bottomLeft, $topPoint),
new Face($bottomLeft, $topLeft, $topPoint),
new Face($topLeft, $topRight, $topPoint),
new Face($topRight, $topLeft, $bottomLeft, $bottomRight),
],
));
}
}

111
src/Shape/Sphere.php Normal file
View File

@ -0,0 +1,111 @@
<?php
namespace Rikudou\PhpScad\Shape;
use JetBrains\PhpStorm\Immutable;
use Rikudou\PhpScad\FacetsConfiguration\FacetsConfiguration;
use Rikudou\PhpScad\Implementation\ConditionalRenderable;
use Rikudou\PhpScad\Implementation\FacetsConfigImplementation;
use Rikudou\PhpScad\Implementation\RenderableImplementation;
use Rikudou\PhpScad\Implementation\ValueConverter;
use Rikudou\PhpScad\Implementation\Wither;
use Rikudou\PhpScad\Module\NonCenterableSphereModule;
use Rikudou\PhpScad\Primitive\HasFacetsConfig;
use Rikudou\PhpScad\Primitive\HasModuleDefinitions;
use Rikudou\PhpScad\Primitive\HasWrappers;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Value\BoolValue;
use Rikudou\PhpScad\Value\NullValue;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
#[Immutable(Immutable::PRIVATE_WRITE_SCOPE)]
final class Sphere implements Renderable, HasFacetsConfig, HasModuleDefinitions, HasWrappers
{
use FacetsConfigImplementation;
use RenderableImplementation;
use ConditionalRenderable;
use Wither;
use ValueConverter;
private Reference|NumericValue|NullValue $radius;
private Reference|NumericValue|NullValue $diameter;
private Reference|BoolValue $center;
public function __construct(
Reference|NumericValue|NullValue|float|null $radius = null,
Reference|NumericValue|NullValue|float|null $diameter = null,
Reference|BoolValue|bool $center = true,
?FacetsConfiguration $facetsConfiguration = null,
) {
$this->facetsConfiguration = $facetsConfiguration;
$this->radius = $this->convertToValue($radius);
$this->diameter = $this->convertToValue($diameter);
$this->center = $this->convertToValue($center);
}
public function getRadius(): Reference|NumericValue|NullValue
{
return $this->radius;
}
public function withRadius(Reference|NumericValue|NullValue|float|null $radius): self
{
return $this->with('radius', $this->convertToValue($radius));
}
public function getDiameter(): Reference|NumericValue|NullValue
{
return $this->diameter;
}
public function withDiameter(Reference|NumericValue|NullValue|float|null $diameter): self
{
return $this->with('diameter', $this->convertToValue($diameter));
}
public function isCentered(): BoolValue
{
return $this->center;
}
public function withCentered(bool|BoolValue $centered): self
{
return $this->with('center', $this->convertToValue($centered));
}
public function getModules(): iterable
{
yield new NonCenterableSphereModule();
}
protected function isRenderable(): bool
{
return (!$this->radius->hasLiteralValue() || (!$this->radius instanceof NullValue && $this->radius->getValue() > 0))
|| (!$this->diameter->hasLiteralValue() || (!$this->diameter instanceof NullValue && $this->diameter->getValue() > 0))
;
}
protected function doRender(): string
{
$content = 'PhpScad_NonCenterableSphere(';
if (!$this->radius instanceof NullValue) {
$parameter = 'r';
} else {
$parameter = 'd';
}
$content .= "{$parameter} = ";
$content .= !$this->radius instanceof NullValue ? $this->radius : $this->diameter;
$content .= ", center = {$this->center}";
if ($facetsParameters = $this->getFacetsParameters()) {
$content .= ", {$facetsParameters}";
}
$content .= ');';
return $content;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Rikudou\PhpScad\Transformation;
use Rikudou\PhpScad\Color\Color;
use Rikudou\PhpScad\Implementation\RenderableImplementation;
use Rikudou\PhpScad\Implementation\WrapperModuleDefinitions;
use Rikudou\PhpScad\Implementation\WrapperRenderableImplementation;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Primitive\WrapperRenderable;
use Rikudou\PhpScad\Wrapper\WrapperConfiguration;
use Rikudou\PhpScad\Wrapper\WrapperValuePlaceholder;
final class ColorChange implements WrapperRenderable
{
use RenderableImplementation;
use WrapperModuleDefinitions;
use WrapperRenderableImplementation;
public function __construct(
?Color $color,
Renderable ...$renderable,
) {
$this->renderables = $renderable;
$this->color = $color;
}
public function getWrappers(): iterable
{
yield new WrapperConfiguration(
Translate::class,
$this->getPosition(),
new WrapperValuePlaceholder(),
);
}
protected function doRender(): string
{
$result = '';
if ($this->color !== null) {
$result .= "color({$this->color}) {";
}
$result .= $this->renderUnwrappedRenderables();
if ($this->color !== null) {
$result .= '}';
}
return $result;
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace Rikudou\PhpScad\Transformation;
use Rikudou\PhpScad\Implementation\RenderableImplementation;
use Rikudou\PhpScad\Implementation\WrapperModuleDefinitions;
use Rikudou\PhpScad\Implementation\WrapperRenderableImplementation;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Primitive\WrapperRenderable;
use Rikudou\PhpScad\Value\Autoscale;
use Rikudou\PhpScad\Value\BoolValue;
use Rikudou\PhpScad\Value\NullValue;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
use Rikudou\PhpScad\Value\VectorValue;
final class Resize implements WrapperRenderable
{
use RenderableImplementation;
use WrapperModuleDefinitions;
use WrapperRenderableImplementation;
private NumericValue|Reference $newWidth;
private NumericValue|Reference $newDepth;
private NumericValue|Reference $newHeight;
private BoolValue|Reference|NullValue|Autoscale|VectorValue $autoscale;
public function __construct(
NumericValue|Reference|float $newWidth = 0,
NumericValue|Reference|float $newDepth = 0,
NumericValue|Reference|float $newHeight = 0,
BoolValue|Reference|NullValue|Autoscale|VectorValue|bool|null|array $autoscale = null,
Renderable ...$renderable,
) {
$this->renderables = $renderable;
$this->newWidth = $this->convertToValue($newWidth);
$this->newDepth = $this->convertToValue($newDepth);
$this->newHeight = $this->convertToValue($newHeight);
$this->autoscale = $this->convertToValue($autoscale);
}
protected function doRender(): string
{
$usefulWrapper =
!$this->newWidth->hasLiteralValue()
|| !$this->newDepth->hasLiteralValue()
|| !$this->newHeight->hasLiteralValue()
|| (float) $this->newWidth->getValue() !== 0.0
|| (float) $this->newDepth->getValue() !== 0.0
|| (float) $this->newHeight->getValue() !== 0.0
;
$result = '';
if ($usefulWrapper) {
$result .= "resize(newsize = [{$this->newWidth}, {$this->newDepth}, {$this->newHeight}]";
if (!$this->autoscale instanceof NullValue) {
$result .= ", auto = {$this->autoscale}";
}
$result .= ') {';
}
$result .= $this->renderRenderables();
if ($usefulWrapper) {
$result .= '}';
}
return $result;
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Rikudou\PhpScad\Transformation;
use Rikudou\PhpScad\Implementation\RenderableImplementation;
use Rikudou\PhpScad\Implementation\WrapperModuleDefinitions;
use Rikudou\PhpScad\Implementation\WrapperRenderableImplementation;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Primitive\WrapperRenderable;
use Rikudou\PhpScad\Value\NullValue;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Point;
use Rikudou\PhpScad\Value\Reference;
use Rikudou\PhpScad\Value\VectorValue;
final class Rotate implements WrapperRenderable
{
use RenderableImplementation;
use WrapperModuleDefinitions;
use WrapperRenderableImplementation;
private VectorValue|NumericValue|Reference $rotation;
private Point|VectorValue|NullValue|Reference $axis;
public function __construct(
VectorValue|NumericValue|Reference|array|float $rotation,
Point|VectorValue|NullValue|Reference|array|null $axis = null,
Renderable ...$renderable,
) {
$this->renderables = $renderable;
$this->rotation = $this->convertToValue($rotation);
$this->axis = $this->convertToValue($axis);
}
public function withRotation(VectorValue|NumericValue|Reference|array|float $rotation): self
{
return $this->with('rotation', $this->convertToValue($rotation));
}
public function withAxis(Point|VectorValue|NullValue|Reference|array|null $axis): self
{
return $this->with('axis', $this->convertToValue($axis));
}
protected function doRender(): string
{
if ($this->rotation instanceof VectorValue && !$this->axis instanceof NullValue) {
error_log(
'Providing axis point when rotation is an array is pointless, the axis will be ignored.',
E_USER_NOTICE,
);
}
return "rotate(a = {$this->rotation}, v = {$this->axis}) {{$this->renderRenderables()}}";
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace Rikudou\PhpScad\Transformation;
use Rikudou\PhpScad\Implementation\RenderableImplementation;
use Rikudou\PhpScad\Implementation\WrapperModuleDefinitions;
use Rikudou\PhpScad\Implementation\WrapperRenderableImplementation;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Primitive\WrapperRenderable;
use Rikudou\PhpScad\Value\NumericValue;
use Rikudou\PhpScad\Value\Reference;
final class Scale implements WrapperRenderable
{
use RenderableImplementation;
use WrapperModuleDefinitions;
use WrapperRenderableImplementation;
private NumericValue|Reference $scaleX;
private NumericValue|Reference $scaleY;
private NumericValue|Reference $scaleZ;
public function __construct(
NumericValue|Reference|float $scaleX = 1,
NumericValue|Reference|float $scaleY = 1,
NumericValue|Reference|float $scaleZ = 1,
Renderable ...$renderable,
) {
$this->renderables = $renderable;
$this->scaleX = $this->convertToValue($scaleX);
$this->scaleY = $this->convertToValue($scaleY);
$this->scaleZ = $this->convertToValue($scaleZ);
}
protected function doRender(): string
{
$usefulWrapper = !$this->scaleX->hasLiteralValue()
|| !$this->scaleY->hasLiteralValue()
|| !$this->scaleZ->hasLiteralValue()
|| (int) $this->scaleX->getValue() !== 1
|| (int) $this->scaleY->getValue() !== 1
|| (int) $this->scaleZ->getValue() !== 1
;
$result = '';
if ($usefulWrapper) {
$result .= "scale([{$this->scaleX}, {$this->scaleY}, {$this->scaleZ}]) {";
}
$result .= $this->renderRenderables();
if ($usefulWrapper) {
$result .= '}';
}
return $result;
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Rikudou\PhpScad\Transformation;
use Rikudou\PhpScad\Coordinate\Coordinate;
use Rikudou\PhpScad\Implementation\RenderableImplementation;
use Rikudou\PhpScad\Implementation\WrapperModuleDefinitions;
use Rikudou\PhpScad\Implementation\WrapperRenderableImplementation;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Primitive\WrapperRenderable;
final class Translate implements WrapperRenderable
{
use RenderableImplementation;
use WrapperModuleDefinitions;
use WrapperRenderableImplementation;
public function __construct(
Coordinate $position,
Renderable ...$renderable,
) {
$this->renderables = $renderable;
$this->position = $position;
}
public function getWrappers(): iterable
{
return [];
}
protected function doRender(): string
{
$x = $this->convertToValue($this->position->getX());
$y = $this->convertToValue($this->position->getY());
$z = $this->convertToValue($this->position->getZ());
$usefulTranslate =
!$x->hasLiteralValue()
|| $x->getValue() !== 0.0
|| !$y->hasLiteralValue()
|| $y->getValue() !== 0.0
|| !$z->hasLiteralValue()
|| $z->getValue() !== 0.0
;
$result = '';
if ($usefulTranslate) {
$result .= "translate([{$x}, {$y}, {$z}]) {";
}
$result .= $this->renderUnwrappedRenderables();
if ($usefulTranslate) {
$result .= '}';
}
return $result;
}
}

30
src/Value/Autoscale.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace Rikudou\PhpScad\Value;
use Rikudou\PhpScad\Implementation\ValueConverter;
final class Autoscale extends Literal
{
use ValueConverter;
public function __construct(
BoolValue|Reference|bool $autoscaleWidth = false,
BoolValue|Reference|bool $autoscaleDepth = false,
BoolValue|Reference|bool $autoscaleHeight = false,
) {
parent::__construct([$autoscaleWidth, $autoscaleDepth, $autoscaleHeight]);
}
public function getValue(): array
{
return parent::getValue();
}
public function getScadRepresentation(): string
{
$value = $this->getValue();
return "[{$this->convertToValue($value[0])}, {$this->convertToValue($value[1])}, {$this->convertToValue($value[2])}]";
}
}

16
src/Value/BoolValue.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace Rikudou\PhpScad\Value;
final class BoolValue extends Literal
{
public function __construct(bool $value)
{
parent::__construct($value);
}
public function getScadRepresentation(): string
{
return $this->getValue() ? 'true' : 'false';
}
}

16
src/Value/Expression.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace Rikudou\PhpScad\Value;
final class Expression extends Reference
{
public function __construct(
private readonly string $expression,
) {
}
public function getScadRepresentation(): string
{
return "({$this->expression})";
}
}

58
src/Value/Face.php Normal file
View File

@ -0,0 +1,58 @@
<?php
namespace Rikudou\PhpScad\Value;
use InvalidArgumentException;
final class Face extends Literal
{
public function __construct(
int|Point ...$point,
) {
parent::__construct($point);
}
/**
* @return array<int|Point>
*/
public function getValue(): array
{
return parent::getValue();
}
public function getScadRepresentation(?PointVector &$points = null): string
{
$value = $this->getValue();
if (count($value) < 3) {
throw new InvalidArgumentException('A polyhedron face must have at least 3 points');
}
$points ??= new PointVector();
$result = '[';
foreach ($value as $point) {
if (is_int($point)) {
$result .= "{$point},";
} else {
$result .= "{$this->getPointIndex($point, $points)},";
}
}
$result = substr($result, 0, -1);
$result .= ']';
return $result;
}
private function getPointIndex(Point $pointToFind, PointVector &$points): int
{
foreach ($points as $index => $point) {
if ($point->getScadRepresentation() === $pointToFind->getScadRepresentation()) {
return $index;
}
}
$points = $points->withPoint($pointToFind);
return $this->getPointIndex($pointToFind, $points);
}
}

36
src/Value/FaceVector.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace Rikudou\PhpScad\Value;
final class FaceVector extends Literal
{
public function __construct(Face ...$face)
{
parent::__construct($face);
}
/**
* @return array<Face>
*/
public function getValue(): array
{
return parent::getValue();
}
public function getScadRepresentation(?PointVector &$points = null): string
{
$value = $this->getValue();
$result = '[';
foreach ($value as $item) {
$result .= $item->getScadRepresentation($points) . ',';
}
if (count($value)) {
$result = substr($result, 0, -1);
}
$result .= ']';
return $result;
}
}

16
src/Value/FloatValue.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace Rikudou\PhpScad\Value;
final class FloatValue extends NumericValue
{
public function __construct(float $value)
{
parent::__construct($value);
}
public function getScadRepresentation(): string
{
return (string) $this->getValue();
}
}

16
src/Value/IntValue.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace Rikudou\PhpScad\Value;
final class IntValue extends NumericValue
{
public function __construct(int $value)
{
parent::__construct($value);
}
public function getScadRepresentation(): string
{
return (string) $this->getValue();
}
}

26
src/Value/Literal.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace Rikudou\PhpScad\Value;
abstract class Literal implements Value
{
public function __construct(
private readonly mixed $value,
) {
}
public function __toString(): string
{
return $this->getScadRepresentation();
}
public function getValue(): mixed
{
return $this->value;
}
public function hasLiteralValue(): bool
{
return true;
}
}

16
src/Value/NullValue.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace Rikudou\PhpScad\Value;
final class NullValue extends Literal
{
public function __construct()
{
parent::__construct(null);
}
public function getScadRepresentation(): string
{
return 'undef';
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Rikudou\PhpScad\Value;
abstract class NumericValue extends Literal
{
public function __construct(int|float $value)
{
parent::__construct($value);
}
public function getValue(): int|float
{
return parent::getValue();
}
}

51
src/Value/Point.php Normal file
View File

@ -0,0 +1,51 @@
<?php
namespace Rikudou\PhpScad\Value;
use Rikudou\PhpScad\Implementation\ValueConverter;
final class Point implements Value
{
use ValueConverter;
private NumericValue|Reference $x;
private NumericValue|Reference $y;
private NumericValue|Reference $z;
public function __construct(
float|NumericValue|Reference $x,
float|NumericValue|Reference $y,
float|NumericValue|Reference $z,
) {
$this->x = $this->convertToValue($x);
$this->y = $this->convertToValue($y);
$this->z = $this->convertToValue($z);
}
public function __toString(): string
{
return $this->getScadRepresentation();
}
public function getValue(): array
{
return [$this->x, $this->y, $this->z];
}
public function getScadRepresentation(): string
{
$value = $this->getValue();
return "[{$value[0]}, {$value[1]}, {$value[2]}]";
}
public function hasLiteralValue(): bool
{
return $this->x->hasLiteralValue()
&& $this->y->hasLiteralValue()
&& $this->z->hasLiteralValue()
;
}
}

66
src/Value/PointVector.php Normal file
View File

@ -0,0 +1,66 @@
<?php
namespace Rikudou\PhpScad\Value;
use ArrayIterator;
use IteratorAggregate;
use Traversable;
final class PointVector extends Literal implements IteratorAggregate
{
public function __construct(
Point ...$point,
) {
parent::__construct($point);
}
/**
* @return array<Point>
*/
public function getValue(): array
{
return parent::getValue();
}
public function getScadRepresentation(): string
{
$value = $this->getValue();
$result = '[';
foreach ($value as $item) {
$result .= $item->getScadRepresentation() . ',';
}
if (count($value)) {
$result = substr($result, 0, -1);
}
$result .= ']';
return $result;
}
public static function fromArray(array $raw): self
{
$points = array_map(function (array|Point $item): Point {
return $item instanceof Point ? $item : new Point(...$item);
}, $raw);
return new self(...$points);
}
/**
* @return Traversable<Point>
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->getValue());
}
public function withPoint(Point $point): self
{
$points = $this->getValue();
$points[] = $point;
return new self(...$points);
}
}

21
src/Value/Reference.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace Rikudou\PhpScad\Value;
abstract class Reference implements Value
{
public function __toString(): string
{
return $this->getScadRepresentation();
}
public function getValue(): mixed
{
return null;
}
public function hasLiteralValue(): bool
{
return false;
}
}

21
src/Value/StringValue.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace Rikudou\PhpScad\Value;
final class StringValue extends Literal
{
public function __construct(string $value)
{
parent::__construct($value);
}
public function getValue(): string
{
return parent::getValue();
}
public function getScadRepresentation(): string
{
return "\"{$this->getValue()}\"";
}
}

14
src/Value/Value.php Normal file
View File

@ -0,0 +1,14 @@
<?php
namespace Rikudou\PhpScad\Value;
use Stringable;
interface Value extends Stringable
{
public function getScadRepresentation(): string;
public function getValue(): mixed;
public function hasLiteralValue(): bool;
}

21
src/Value/Variable.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace Rikudou\PhpScad\Value;
final class Variable extends Reference
{
public function __construct(
private readonly string $variableName,
) {
}
public function getScadRepresentation(): string
{
$name = $this->variableName;
if (!str_starts_with($name, '$')) {
$name = "\${$name}";
}
return $name;
}
}

37
src/Value/VectorValue.php Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace Rikudou\PhpScad\Value;
use Rikudou\PhpScad\Implementation\ValueConverter;
final class VectorValue extends Literal
{
use ValueConverter;
public function __construct(array $value)
{
parent::__construct($value);
}
public function getValue(): array
{
return parent::getValue();
}
public function getScadRepresentation(): string
{
$value = $this->getValue();
$result = '[';
foreach ($value as $item) {
$result .= $this->convertToValue($item)->getScadRepresentation() . ',';
}
if (count($value)) {
$result = substr($result, 0, -1);
}
$result .= ']';
return $result;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Rikudou\PhpScad\Wrapper;
use Rikudou\PhpScad\Implementation\RenderableImplementation;
use Rikudou\PhpScad\Implementation\WrapperModuleDefinitions;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Primitive\WrapperRenderable;
final class CommentWrapper implements WrapperRenderable
{
use RenderableImplementation;
use WrapperModuleDefinitions;
public function __construct(
private readonly string $comment,
private readonly Renderable $renderable,
) {
}
public function render(): string
{
return "/* {$this->comment} */ {$this->renderable->render()}";
}
protected function getRenderables(): iterable
{
yield $this->renderable;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Rikudou\PhpScad\Wrapper;
use Rikudou\PhpScad\Primitive\Renderable;
use Rikudou\PhpScad\Primitive\WrapperRenderable;
final class WrapperConfiguration
{
private readonly array $arguments;
/**
* @param class-string<WrapperRenderable> $class
*/
public function __construct(
public readonly string $class,
mixed ...$arguments,
) {
$this->arguments = $arguments;
}
public function getArguments(Renderable $renderable): array
{
$arguments = $this->arguments;
foreach ($arguments as $key => $value) {
if ($value instanceof WrapperValuePlaceholder) {
$arguments[$key] = $renderable;
}
}
return $arguments;
}
}

Some files were not shown because too many files have changed in this diff Show More