Initial commit
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/.idea
|
||||
/vendor
|
||||
/composer.lock
|
||||
/test.php
|
||||
/.php-cs-fixer.cache
|
122
.php-cs-fixer.dist.php
Normal 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
@ -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
@ -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
After Width: | Height: | Size: 7.6 KiB |
BIN
doc/img/example-cylinder-centered.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
doc/img/example-cylinder-cone.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
doc/img/example-cylinder.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
doc/img/example-manual-translate.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
doc/img/example-openscad.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
doc/img/example-php-openscad.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
doc/img/example-polyhedron.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
doc/img/example-pyramid.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
doc/img/example-sphere-non-centered.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
doc/img/example-sphere.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
doc/img/sphere-facets-default.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
doc/img/sphere-fn-120.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
doc/img/sphere-fn-240.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
doc/img/sphere-fn-360.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
doc/img/sphere-fn-60.png
Normal file
After Width: | Height: | Size: 22 KiB |
2
phpstan.neon.dist
Normal file
@ -0,0 +1,2 @@
|
||||
parameters:
|
||||
inferPrivatePropertyTypeFromConstructor: true
|
11
src/Color/AbstractColor.php
Normal 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
@ -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
@ -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
@ -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
@ -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}";
|
||||
}
|
||||
}
|
27
src/Combination/Difference.php
Normal 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()}}";
|
||||
}
|
||||
}
|
27
src/Combination/RenderableContainer.php
Normal 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();
|
||||
}
|
||||
}
|
40
src/Coordinate/AbstractCoordinate.php
Normal 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}");
|
||||
}
|
||||
}
|
17
src/Coordinate/Coordinate.php
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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;
|
||||
}
|
||||
}
|
21
src/Coordinate/ZeroCoordinate.php
Normal 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;
|
||||
}
|
||||
}
|
74
src/Customizer/AbstractCustomizerVariable.php
Normal 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;
|
||||
}
|
||||
}
|
19
src/Customizer/AbstractNumericCustomizerVariable.php
Normal 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();
|
||||
}
|
||||
}
|
16
src/Customizer/BoolCustomizerVariable.php
Normal 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();
|
||||
}
|
||||
}
|
16
src/Customizer/FloatCustomizerVariable.php
Normal 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();
|
||||
}
|
||||
}
|
16
src/Customizer/IntCustomizerVariable.php
Normal 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();
|
||||
}
|
||||
}
|
11
src/Customizer/NullCustomizerVariable.php
Normal 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);
|
||||
}
|
||||
}
|
7
src/Customizer/NumericCustomizerVariable.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Rikudou\PhpScad\Customizer;
|
||||
|
||||
final class NumericCustomizerVariable extends AbstractNumericCustomizerVariable
|
||||
{
|
||||
}
|
27
src/FacetsConfiguration/FacetsAngleAndSize.php
Normal 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;
|
||||
}
|
||||
}
|
12
src/FacetsConfiguration/FacetsConfiguration.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Rikudou\PhpScad\FacetsConfiguration;
|
||||
|
||||
interface FacetsConfiguration
|
||||
{
|
||||
public function getMinimumFragmentAngle(): ?float;
|
||||
|
||||
public function getMinimumFragmentSize(): ?float;
|
||||
|
||||
public function getNumberOfFragments(): ?float;
|
||||
}
|
26
src/FacetsConfiguration/FacetsNumber.php
Normal 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;
|
||||
}
|
||||
}
|
30
src/Implementation/AliasShape.php
Normal 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();
|
||||
}
|
||||
}
|
19
src/Implementation/ConditionalRenderable.php
Normal 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;
|
||||
}
|
44
src/Implementation/FacetsConfigImplementation.php
Normal 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;
|
||||
}
|
||||
}
|
22
src/Implementation/GetWrappedRenderable.php
Normal 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;
|
||||
}
|
||||
}
|
103
src/Implementation/RenderableImplementation.php
Normal 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);
|
||||
}
|
||||
}
|
119
src/Implementation/ValueConverter.php
Normal 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;
|
||||
}
|
||||
}
|
14
src/Implementation/Wither.php
Normal 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;
|
||||
}
|
||||
}
|
23
src/Implementation/WrapperModuleDefinitions.php
Normal 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;
|
||||
}
|
59
src/Implementation/WrapperRenderableImplementation.php
Normal 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;
|
||||
}
|
||||
}
|
18
src/Module/CustomizerEndModule.php
Normal 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()}() {}";
|
||||
}
|
||||
}
|
31
src/Module/NonCenterableCylinderModule.php
Normal 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);
|
||||
}";
|
||||
}
|
||||
}
|
22
src/Module/NonCenterableSphereModule.php
Normal 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);
|
||||
}";
|
||||
}
|
||||
}
|
16
src/Primitive/CustomizerVariable.php
Normal 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;
|
||||
}
|
12
src/Primitive/HasFacetsConfig.php
Normal 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;
|
||||
}
|
11
src/Primitive/HasModuleDefinitions.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Rikudou\PhpScad\Primitive;
|
||||
|
||||
interface HasModuleDefinitions
|
||||
{
|
||||
/**
|
||||
* @return iterable<Module>
|
||||
*/
|
||||
public function getModules(): iterable;
|
||||
}
|
13
src/Primitive/HasWrappers.php
Normal 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
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Rikudou\PhpScad\Primitive;
|
||||
|
||||
interface Module
|
||||
{
|
||||
public function getName(): string;
|
||||
|
||||
public function getDefinition(): string;
|
||||
}
|
19
src/Primitive/Renderable.php
Normal 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;
|
||||
}
|
7
src/Primitive/WrapperRenderable.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Rikudou\PhpScad\Primitive;
|
||||
|
||||
interface WrapperRenderable extends Renderable, HasModuleDefinitions, HasWrappers
|
||||
{
|
||||
}
|
64
src/Renderer/AbstractOpenScadBinaryRenderer.php
Normal 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];
|
||||
}
|
||||
}
|
31
src/Renderer/PngPreviewRenderer.php
Normal 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;
|
||||
}
|
||||
}
|
8
src/Renderer/Renderer.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Rikudou\PhpScad\Renderer;
|
||||
|
||||
interface Renderer
|
||||
{
|
||||
public function render(string $outputFile, string $scadContent): void;
|
||||
}
|
11
src/Renderer/ScadFileRenderer.php
Normal 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);
|
||||
}
|
||||
}
|
21
src/Renderer/StlRenderer.php
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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;
|
||||
}
|
||||
}
|
50
src/Transformation/ColorChange.php
Normal 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;
|
||||
}
|
||||
}
|
77
src/Transformation/Resize.php
Normal 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;
|
||||
}
|
||||
}
|
58
src/Transformation/Rotate.php
Normal 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()}}";
|
||||
}
|
||||
}
|
59
src/Transformation/Scale.php
Normal 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;
|
||||
}
|
||||
}
|
57
src/Transformation/Translate.php
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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';
|
||||
}
|
||||
}
|
16
src/Value/NumericValue.php
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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;
|
||||
}
|
||||
}
|
30
src/Wrapper/CommentWrapper.php
Normal 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;
|
||||
}
|
||||
}
|
34
src/Wrapper/WrapperConfiguration.php
Normal 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;
|
||||
}
|
||||
}
|