summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--icons/license.txt3
-rw-r--r--icons/prefsFeatures.pngbin0 -> 6099 bytes
-rw-r--r--openscad.pro2
-rw-r--r--openscad.qrc1
-rw-r--r--src/Preferences.cc115
-rw-r--r--src/Preferences.h5
-rw-r--r--src/Preferences.ui92
-rw-r--r--src/feature.cc79
-rw-r--r--src/feature.h44
-rw-r--r--src/func.cc19
-rw-r--r--src/function.h9
-rw-r--r--src/modcontext.cc11
-rw-r--r--src/module.h7
-rw-r--r--src/openscad.cc10
-rw-r--r--testdata/scad/experimental/concat-tests.scad53
-rw-r--r--tests/CMakeLists.txt6
-rw-r--r--tests/regression/echotest/concat-tests-expected.echo43
18 files changed, 483 insertions, 18 deletions
diff --git a/.gitignore b/.gitignore
index 59bac49..040fe53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,6 @@ parser_yacc.h
/tmp
/OpenSCAD.app
*/#*#
+/nbproject
/openscad
+/tests/openscad_nogui
diff --git a/icons/license.txt b/icons/license.txt
new file mode 100644
index 0000000..97e6931
--- /dev/null
+++ b/icons/license.txt
@@ -0,0 +1,3 @@
+Taken from http://tango.freedesktop.org/Tango_Icon_Library, version 0.8.90 / public domain:
+
+- prefsFeatures.png (converted from preferences-system.svg)
diff --git a/icons/prefsFeatures.png b/icons/prefsFeatures.png
new file mode 100644
index 0000000..2fb0a55
--- /dev/null
+++ b/icons/prefsFeatures.png
Binary files differ
diff --git a/openscad.pro b/openscad.pro
index 5c39928..20dc299 100644
--- a/openscad.pro
+++ b/openscad.pro
@@ -222,6 +222,7 @@ HEADERS += src/typedefs.h \
src/highlighter.h \
src/localscope.h \
src/module.h \
+ src/feature.h \
src/node.h \
src/csgnode.h \
src/linearextrudenode.h \
@@ -280,6 +281,7 @@ SOURCES += src/version_check.cc \
src/func.cc \
src/localscope.cc \
src/module.cc \
+ src/feature.cc \
src/node.cc \
src/context.cc \
src/modcontext.cc \
diff --git a/openscad.qrc b/openscad.qrc
index 28b6a72..6fd7e47 100644
--- a/openscad.qrc
+++ b/openscad.qrc
@@ -1,5 +1,6 @@
<RCC>
<qresource prefix="/">
+ <file>icons/prefsFeatures.png</file>
<file>icons/stopbutton.png</file>
<file>icons/prefsAdvanced.png</file>
<file>icons/prefs3DView.png</file>
diff --git a/src/Preferences.cc b/src/Preferences.cc
index eed877d..b63ca7b 100644
--- a/src/Preferences.cc
+++ b/src/Preferences.cc
@@ -33,12 +33,16 @@
#include <QStatusBar>
#include "PolySetCache.h"
#include "AutoUpdater.h"
+#include "feature.h"
#ifdef ENABLE_CGAL
#include "CGALCache.h"
#endif
Preferences *Preferences::instance = NULL;
+const char * Preferences::featurePropertyName = "FeatureProperty";
+Q_DECLARE_METATYPE(Feature *);
+
Preferences::Preferences(QWidget *parent) : QMainWindow(parent)
{
setupUi(this);
@@ -89,10 +93,11 @@ Preferences::Preferences(QWidget *parent) : QMainWindow(parent)
// Toolbar
QActionGroup *group = new QActionGroup(this);
- group->addAction(prefsAction3DView);
- group->addAction(prefsActionEditor);
- group->addAction(prefsActionUpdate);
- group->addAction(prefsActionAdvanced);
+ addPrefPage(group, prefsAction3DView, page3DView);
+ addPrefPage(group, prefsActionEditor, pageEditor);
+ addPrefPage(group, prefsActionUpdate, pageUpdate);
+ addPrefPage(group, prefsActionFeatures, pageFeatures);
+ addPrefPage(group, prefsActionAdvanced, pageAdvanced);
connect(group, SIGNAL(triggered(QAction*)), this, SLOT(actionTriggered(QAction*)));
prefsAction3DView->setChecked(true);
@@ -140,6 +145,7 @@ Preferences::Preferences(QWidget *parent) : QMainWindow(parent)
this->polysetCacheSizeEdit->setValidator(validator);
this->opencsgLimitEdit->setValidator(validator);
+ setupFeaturesPage();
updateGUI();
RenderSettings::inst()->setColors(this->colorschemes[getValue("3dview/colorscheme").toString()]);
@@ -150,21 +156,103 @@ Preferences::~Preferences()
removeDefaultSettings();
}
+/**
+ * Add a page for the preferences GUI. This handles both the action grouping
+ * and the registration of the widget for each action to have a generalized
+ * callback to switch pages.
+ *
+ * @param group The action group for all page actions. This one will have the
+ * callback attached after creating all actions/pages.
+ * @param action The action specific for the added page.
+ * @param widget The widget that should be shown when the action is triggered.
+ * This must be a child page of the stackedWidget.
+ */
+void
+Preferences::addPrefPage(QActionGroup *group, QAction *action, QWidget *widget)
+{
+ group->addAction(action);
+ prefPages[action] = widget;
+}
+
+/**
+ * Callback to switch pages in the preferences GUI.
+ *
+ * @param action The action triggered by the user.
+ */
void
Preferences::actionTriggered(QAction *action)
{
- if (action == this->prefsAction3DView) {
- this->stackedWidget->setCurrentWidget(this->page3DView);
- }
- else if (action == this->prefsActionEditor) {
- this->stackedWidget->setCurrentWidget(this->pageEditor);
+ this->stackedWidget->setCurrentWidget(prefPages[action]);
+}
+
+/**
+ * Callback for the dynamically created checkboxes on the features
+ * page. The specific Feature object is associated as property with
+ * the callback.
+ *
+ * @param state the state of the checkbox.
+ */
+void Preferences::featuresCheckBoxToggled(bool state)
+{
+ const QObject *sender = QObject::sender();
+ if (sender == NULL) {
+ return;
}
- else if (action == this->prefsActionUpdate) {
- this->stackedWidget->setCurrentWidget(this->pageUpdate);
+ QVariant v = sender->property(featurePropertyName);
+ if (!v.isValid()) {
+ return;
}
- else if (action == this->prefsActionAdvanced) {
- this->stackedWidget->setCurrentWidget(this->pageAdvanced);
+ Feature *feature = v.value<Feature *>();
+ feature->enable(state);
+ QSettings settings;
+ settings.setValue(QString("feature/%1").arg(QString::fromStdString(feature->get_name())), state);
+}
+
+/**
+ * Setup feature GUI and synchronize the Qt settings with the feature values.
+ *
+ * In case a feature was enabled on the commandline this will have precedence
+ * and cause the checkbox in the settings GUI to be not editable.
+ * Otherwise the value from the Qt settings is pushed into the feature state
+ * and the checkbox is initialized accordingly.
+ */
+void
+Preferences::setupFeaturesPage()
+{
+ int row = 0;
+ for (Feature::iterator it = Feature::begin();it != Feature::end();it++) {
+ Feature *feature = *it;
+
+ QString featurekey = QString("feature/%1").arg(QString::fromStdString(feature->get_name()));
+ this->defaultmap[featurekey] = false;
+
+ // spacer item between the features, just for some optical separation
+ gridLayoutExperimentalFeatures->addItem(new QSpacerItem(1, 8, QSizePolicy::Expanding, QSizePolicy::Fixed), row, 1, 1, 1, Qt::AlignCenter);
+ row++;
+
+ QCheckBox *cb = new QCheckBox(QString::fromStdString(feature->get_name()), pageFeatures);
+ QFont bold_font(cb->font());
+ bold_font.setBold(true);
+ cb->setFont(bold_font);
+ // synchronize Qt settings with the feature settings
+ bool value = getValue(featurekey).toBool();
+ feature->enable(value);
+ cb->setChecked(value);
+ cb->setProperty(featurePropertyName, QVariant::fromValue<Feature *>(feature));
+ connect(cb, SIGNAL(toggled(bool)), this, SLOT(featuresCheckBoxToggled(bool)));
+ gridLayoutExperimentalFeatures->addWidget(cb, row, 0, 1, 2, Qt::AlignLeading);
+ row++;
+
+ QLabel *l = new QLabel(QString::fromStdString(feature->get_description()), pageFeatures);
+ l->setTextFormat(Qt::RichText);
+ gridLayoutExperimentalFeatures->addWidget(l, row, 1, 1, 1, Qt::AlignLeading);
+ row++;
}
+ // Force fixed indentation, the checkboxes use column span of 2 so
+ // first row is not constrained in size by the visible controls. The
+ // fixed size space essentially gives the first row the width of the
+ // spacer item itself.
+ gridLayoutExperimentalFeatures->addItem(new QSpacerItem(20, 0, QSizePolicy::Fixed, QSizePolicy::Fixed), 1, 0, 1, 1, Qt::AlignLeading);
}
void Preferences::on_colorSchemeChooser_itemSelectionChanged()
@@ -316,7 +404,6 @@ QVariant Preferences::getValue(const QString &key) const
void Preferences::updateGUI()
{
- QSettings settings;
QList<QListWidgetItem *> found =
this->colorSchemeChooser->findItems(getValue("3dview/colorscheme").toString(),
Qt::MatchExactly);
diff --git a/src/Preferences.h b/src/Preferences.h
index 1230c8a..d74ada9 100644
--- a/src/Preferences.h
+++ b/src/Preferences.h
@@ -21,6 +21,7 @@ public:
public slots:
void actionTriggered(class QAction *);
+ void featuresCheckBoxToggled(bool);
void on_colorSchemeChooser_itemSelectionChanged();
void on_fontChooser_activated(const QString &);
void on_fontSize_editTextChanged(const QString &);
@@ -46,11 +47,15 @@ private:
void keyPressEvent(QKeyEvent *e);
void updateGUI();
void removeDefaultSettings();
+ void setupFeaturesPage();
+ void addPrefPage(QActionGroup *group, QAction *action, QWidget *widget);
QSettings::SettingsMap defaultmap;
QHash<QString, std::map<RenderSettings::RenderColor, Color4f> > colorschemes;
+ QHash<const QAction *, QWidget *> prefPages;
static Preferences *instance;
+ static const char *featurePropertyName;
};
#endif
diff --git a/src/Preferences.ui b/src/Preferences.ui
index ea039f2..100bcb1 100644
--- a/src/Preferences.ui
+++ b/src/Preferences.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>473</width>
- <height>320</height>
+ <width>823</width>
+ <height>433</height>
</rect>
</property>
<property name="sizePolicy">
@@ -378,6 +378,78 @@
</item>
</layout>
</widget>
+ <widget class="QWidget" name="pageFeatures">
+ <layout class="QGridLayout" name="gridLayout_2">
+ <property name="margin">
+ <number>0</number>
+ </property>
+ <item row="0" column="0">
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <item>
+ <widget class="QLabel" name="label_9">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Features</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Plain</enum>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="scrollAreaWidgetContents">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>803</width>
+ <height>311</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <item>
+ <layout class="QGridLayout" name="gridLayoutExperimentalFeatures" rowminimumheight="0">
+ <property name="spacing">
+ <number>8</number>
+ </property>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>282</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
<widget class="QWidget" name="pageAdvanced">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
@@ -520,6 +592,7 @@
<addaction name="prefsAction3DView"/>
<addaction name="prefsActionEditor"/>
<addaction name="prefsActionUpdate"/>
+ <addaction name="prefsActionFeatures"/>
<addaction name="prefsActionAdvanced"/>
</widget>
<action name="prefsAction3DView">
@@ -570,6 +643,21 @@
<string>Update</string>
</property>
</action>
+ <action name="prefsActionFeatures">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="icon">
+ <iconset resource="../openscad.qrc">
+ <normaloff>:/icons/prefsFeatures.png</normaloff>:/icons/prefsFeatures.png</iconset>
+ </property>
+ <property name="text">
+ <string>Features</string>
+ </property>
+ <property name="toolTip">
+ <string>Enable/Disable experimental features</string>
+ </property>
+ </action>
</widget>
<resources>
<include location="../openscad.qrc"/>
diff --git a/src/feature.cc b/src/feature.cc
new file mode 100644
index 0000000..80d7887
--- /dev/null
+++ b/src/feature.cc
@@ -0,0 +1,79 @@
+#include <stdio.h>
+#include <iostream>
+#include <string>
+#include <map>
+
+#include "feature.h"
+#include "printutils.h"
+
+/**
+ * Feature registration map/list for later lookup. This must be initialized
+ * before the static feature instances as those register with this map.
+ */
+Feature::map_t Feature::feature_map;
+Feature::list_t Feature::feature_list;
+
+/*
+ * List of features, the names given here are used in both command line
+ * argument to enable the option and for saving the option value in GUI
+ * context.
+ */
+const Feature Feature::ExperimentalConcatFunction("concat", "Enable the <code>concat()</code> function.");
+
+Feature::Feature(const std::string &name, const std::string &description)
+ : enabled(false), name(name), description(description)
+{
+ feature_map[name] = this;
+ feature_list.push_back(this);
+}
+
+Feature::~Feature()
+{
+}
+
+const std::string &Feature::get_name() const
+{
+ return name;
+}
+
+const std::string &Feature::get_description() const
+{
+ return description;
+}
+
+bool Feature::is_enabled() const
+{
+ return enabled;
+}
+
+void Feature::enable(bool status)
+{
+ enabled = status;
+}
+
+void Feature::enable_feature(const std::string &feature_name, bool status)
+{
+ map_t::iterator it = feature_map.find(feature_name);
+ if (it != feature_map.end()) {
+ it->second->enable(status);
+ } else {
+ PRINTB("WARNING: Ignoring request to enable unknown feature '%s'.", feature_name);
+ }
+}
+
+Feature::iterator Feature::begin()
+{
+ return feature_list.begin();
+}
+
+Feature::iterator Feature::end()
+{
+ return feature_list.end();
+}
+
+void Feature::dump_features()
+{
+ for (map_t::iterator it = feature_map.begin(); it != feature_map.end(); it++) {
+ std::cout << "Feature('" << it->first << "') = " << (it->second->is_enabled() ? "enabled" : "disabled") << std::endl;
+ }
+}
diff --git a/src/feature.h b/src/feature.h
new file mode 100644
index 0000000..20b4f16
--- /dev/null
+++ b/src/feature.h
@@ -0,0 +1,44 @@
+#ifndef FEATURE_H_
+#define FEATURE_H_
+
+#include <stdio.h>
+#include <iostream>
+#include <string>
+#include <map>
+#include <vector>
+
+class Feature
+{
+public:
+ typedef std::vector<Feature *> list_t;
+ typedef list_t::iterator iterator;
+
+ static const Feature ExperimentalConcatFunction;
+
+ const std::string& get_name() const;
+ const std::string& get_description() const;
+
+ bool is_enabled() const;
+ void enable(bool status);
+
+ static iterator begin();
+ static iterator end();
+
+ static void dump_features();
+ static void enable_feature(const std::string &feature_name, bool status = true);
+
+private:
+ bool enabled;
+
+ const std::string name;
+ const std::string description;
+
+ typedef std::map<std::string, Feature *> map_t;
+ static map_t feature_map;
+ static list_t feature_list;
+
+ Feature(const std::string &name, const std::string &description);
+ virtual ~Feature();
+};
+
+#endif
diff --git a/src/func.cc b/src/func.cc
index 4587f72..878bc1c 100644
--- a/src/func.cc
+++ b/src/func.cc
@@ -343,6 +343,24 @@ Value builtin_str(const Context *, const EvalContext *evalctx)
return Value(stream.str());
}
+Value builtin_concat(const Context *, const EvalContext *evalctx)
+{
+ Value::VectorType result;
+
+ for (size_t i = 0; i < evalctx->numArgs(); i++) {
+ const Value v = evalctx->getArgValue(i);
+ if (v.type() == Value::VECTOR) {
+ Value::VectorType vec = v.toVector();
+ for (Value::VectorType::const_iterator it = vec.begin(); it != vec.end(); it++) {
+ result.push_back(*it);
+ }
+ } else {
+ result.push_back(v);
+ }
+ }
+ return Value(result);
+}
+
Value builtin_lookup(const Context *, const EvalContext *evalctx)
{
double p, low_p, low_v, high_p, high_v;
@@ -604,6 +622,7 @@ void register_builtin_functions()
Builtins::init("log", new BuiltinFunction(&builtin_log));
Builtins::init("ln", new BuiltinFunction(&builtin_ln));
Builtins::init("str", new BuiltinFunction(&builtin_str));
+ Builtins::init("concat", new BuiltinFunction(&builtin_concat, Feature::ExperimentalConcatFunction));
Builtins::init("lookup", new BuiltinFunction(&builtin_lookup));
Builtins::init("search", new BuiltinFunction(&builtin_search));
Builtins::init("version", new BuiltinFunction(&builtin_version));
diff --git a/src/function.h b/src/function.h
index a1fde3c..2491809 100644
--- a/src/function.h
+++ b/src/function.h
@@ -3,13 +3,21 @@
#include "value.h"
#include "typedefs.h"
+#include "feature.h"
+
#include <string>
#include <vector>
+
class AbstractFunction
{
+private:
+ const Feature *feature;
public:
+ AbstractFunction() : feature(NULL) {}
+ AbstractFunction(const Feature& feature) : feature(&feature) {}
virtual ~AbstractFunction();
+ virtual bool is_enabled() const { return (feature == NULL) || feature->is_enabled(); };
virtual Value evaluate(const class Context *ctx, const class EvalContext *evalctx) const;
virtual std::string dump(const std::string &indent, const std::string &name) const;
};
@@ -21,6 +29,7 @@ public:
eval_func_t eval_func;
BuiltinFunction(eval_func_t f) : eval_func(f) { }
+ BuiltinFunction(eval_func_t f, const Feature& feature) : AbstractFunction(feature), eval_func(f) { }
virtual ~BuiltinFunction();
virtual Value evaluate(const Context *ctx, const EvalContext *evalctx) const;
diff --git a/src/modcontext.cc b/src/modcontext.cc
index 7941cf5..21b04fd 100644
--- a/src/modcontext.cc
+++ b/src/modcontext.cc
@@ -95,7 +95,12 @@ void ModuleContext::registerBuiltin()
const AbstractFunction *ModuleContext::findLocalFunction(const std::string &name) const
{
if (this->functions_p && this->functions_p->find(name) != this->functions_p->end()) {
- return this->functions_p->find(name)->second;
+ AbstractFunction *f = this->functions_p->find(name)->second;
+ if (!f->is_enabled()) {
+ PRINTB("WARNING: Experimental builtin function '%s' is not enabled.", name);
+ return NULL;
+ }
+ return f;
}
return NULL;
}
@@ -104,6 +109,10 @@ const AbstractModule *ModuleContext::findLocalModule(const std::string &name) co
{
if (this->modules_p && this->modules_p->find(name) != this->modules_p->end()) {
AbstractModule *m = this->modules_p->find(name)->second;
+ if (!m->is_enabled()) {
+ PRINTB("WARNING: Experimental builtin module '%s' is not enabled.", name);
+ return NULL;
+ }
std::string replacement = Builtins::instance()->isDeprecated(name);
if (!replacement.empty()) {
PRINTB("DEPRECATED: The %s() module will be removed in future releases. Use %s() instead.", name % replacement);
diff --git a/src/module.h b/src/module.h
index 8414706..81e5f10 100644
--- a/src/module.h
+++ b/src/module.h
@@ -13,6 +13,7 @@
#include "value.h"
#include "typedefs.h"
#include "localscope.h"
+#include "feature.h"
class ModuleInstantiation
{
@@ -60,8 +61,13 @@ public:
class AbstractModule
{
+private:
+ const Feature *feature;
public:
+ AbstractModule() : feature(NULL) {}
+ AbstractModule(const Feature& feature) : feature(&feature) {}
virtual ~AbstractModule();
+ virtual bool is_enabled() const { return (feature == NULL) || feature->is_enabled(); };
virtual class AbstractNode *instantiate(const Context *ctx, const ModuleInstantiation *inst, const class EvalContext *evalctx = NULL) const;
virtual std::string dump(const std::string &indent, const std::string &name) const;
};
@@ -70,6 +76,7 @@ class Module : public AbstractModule
{
public:
Module() { }
+ Module(const Feature& feature) : AbstractModule(feature) { }
virtual ~Module();
virtual AbstractNode *instantiate(const Context *ctx, const ModuleInstantiation *inst, const EvalContext *evalctx = NULL) const;
diff --git a/src/openscad.cc b/src/openscad.cc
index ab84235..16a6e89 100644
--- a/src/openscad.cc
+++ b/src/openscad.cc
@@ -33,6 +33,7 @@
#include "builtin.h"
#include "printutils.h"
#include "handle_dep.h"
+#include "feature.h"
#include "parsersettings.h"
#include "rendersettings.h"
#include "PlatformUtils.h"
@@ -111,6 +112,7 @@ static void help(const char *progname)
"%2% --camera=eyex,y,z,centerx,y,z ] \\\n"
"%2%[ --imgsize=width,height ] [ --projection=(o)rtho|(p)ersp] \\\n"
"%2%[ --render | --preview[=throwntogether] ] \\\n"
+ "%2%[ --enable=<feature> \\\n"
"%2%filename\n",
progname % (const char *)tabstr);
exit(1);
@@ -587,7 +589,8 @@ int main(int argc, char **argv)
("x,x", po::value<string>(), "dxf-file")
("d,d", po::value<string>(), "deps-file")
("m,m", po::value<string>(), "makefile")
- ("D,D", po::value<vector<string> >(), "var=val");
+ ("D,D", po::value<vector<string> >(), "var=val")
+ ("enable", po::value<vector<string> >(), "enable experimental features");
po::options_description hidden("Hidden options");
hidden.add_options()
@@ -651,6 +654,11 @@ int main(int argc, char **argv)
commandline_commands += ";\n";
}
}
+ if (vm.count("enable")) {
+ BOOST_FOREACH(const string &feature, vm["enable"].as<vector<string> >()) {
+ Feature::enable_feature(feature);
+ }
+ }
vector<string> inputFiles;
if (vm.count("input-file")) {
inputFiles = vm["input-file"].as<vector<string> >();
diff --git a/testdata/scad/experimental/concat-tests.scad b/testdata/scad/experimental/concat-tests.scad
new file mode 100644
index 0000000..0bcb903
--- /dev/null
+++ b/testdata/scad/experimental/concat-tests.scad
@@ -0,0 +1,53 @@
+u = undef;
+
+echo("--- empty");
+echo(concat());
+echo(concat([]));
+echo(concat([], []));
+echo(concat([], [], []));
+
+echo("--- single elements");
+echo(concat(u));
+echo(concat(true));
+echo(concat(3));
+echo(concat("abc"));
+echo(concat([0:1:10]));
+
+echo("--- single vectors");
+echo(concat([1, 2, 3]));
+echo(concat([[1, 2, 3]]));
+echo(concat([[[1, 2, 3]]]));
+echo(concat([[[1, 2, [3, 4], 5]]]));
+
+echo("--- multiple elements");
+echo(concat(3, 3));
+echo(concat(1, 2, 3));
+echo(concat(1, 2, 3, 4, 5));
+echo(concat(1, "text", false, [1:0.5:3]));
+
+echo("--- vector / element");
+echo(concat([3, 4], u));
+echo(concat([3, 4, 5], 6));
+echo(concat([3, 4, 5, 6], true));
+echo(concat([3, 4, "5", 6], "test"));
+echo(concat([3, 4, true, 6], [4:1:3]));
+
+echo("--- element / vector");
+echo(concat(3, []));
+echo(concat(3, [3, 4]));
+echo(concat(true, [3, [4]]));
+echo(concat("9", [1, 2, 3]));
+echo(concat([6:2:9], [3, [4]]));
+
+echo("--- vector / vector");
+echo(concat([], [3, 4]));
+echo(concat([[]], [3, 4]));
+echo(concat([[2, 4]], [3, 4]));
+echo(concat([5, 6], ["d", [3, 4]]));
+echo(concat([[1, 0, 0], [2, 0, 0]], [3, 0, 0]));
+echo(concat([[1, 0, 0], [2, 0, 0]], [[3, 0, 0]]));
+echo(concat([[1, 0, 0], [2, 0, 0], [3, 0, 0]], [[4, 4, 4], [5, 5, 5]]));
+
+echo("--- recursive function");
+function r(i) = i > 0 ? concat(r(i - 1), [[i, i * i]]) : [];
+echo(r(10));
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 376b874..306f063 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -517,6 +517,7 @@ set(CORE_SOURCES
../src/context.cc
../src/modcontext.cc
../src/evalcontext.cc
+ ../src/feature.cc
../src/csgterm.cc
../src/csgtermnormalizer.cc
../src/polyset.cc
@@ -990,6 +991,11 @@ add_cmdline_test(throwntogethertest EXE ${OPENSCAD_BINPATH} ARGS --preview=throw
# with anything. It's self-contained and returns != 0 on error
add_cmdline_test(cgalstlsanitytest EXE ${CMAKE_SOURCE_DIR}/cgalstlsanitytest SUFFIX txt ARGS ${OPENSCAD_BINPATH} FILES ${CGALSTLSANITYTEST_FILES})
+# Add experimental tests
+
+add_cmdline_test(echotest EXE ${OPENSCAD_BINPATH} ARGS --enable=concat -o SUFFIX echo FILES ${CMAKE_SOURCE_DIR}/../testdata/scad/experimental/concat-tests.scad)
+
+
# Tests using the actual OpenSCAD binary
# non-ASCII filenames
diff --git a/tests/regression/echotest/concat-tests-expected.echo b/tests/regression/echotest/concat-tests-expected.echo
new file mode 100644
index 0000000..58b41d0
--- /dev/null
+++ b/tests/regression/echotest/concat-tests-expected.echo
@@ -0,0 +1,43 @@
+ECHO: "--- empty"
+ECHO: []
+ECHO: []
+ECHO: []
+ECHO: []
+ECHO: "--- single elements"
+ECHO: [undef]
+ECHO: [true]
+ECHO: [3]
+ECHO: ["abc"]
+ECHO: [[0 : 1 : 10]]
+ECHO: "--- single vectors"
+ECHO: [1, 2, 3]
+ECHO: [[1, 2, 3]]
+ECHO: [[[1, 2, 3]]]
+ECHO: [[[1, 2, [3, 4], 5]]]
+ECHO: "--- multiple elements"
+ECHO: [3, 3]
+ECHO: [1, 2, 3]
+ECHO: [1, 2, 3, 4, 5]
+ECHO: [1, "text", false, [1 : 0.5 : 3]]
+ECHO: "--- vector / element"
+ECHO: [3, 4, undef]
+ECHO: [3, 4, 5, 6]
+ECHO: [3, 4, 5, 6, true]
+ECHO: [3, 4, "5", 6, "test"]
+ECHO: [3, 4, true, 6, [4 : 1 : 3]]
+ECHO: "--- element / vector"
+ECHO: [3]
+ECHO: [3, 3, 4]
+ECHO: [true, 3, [4]]
+ECHO: ["9", 1, 2, 3]
+ECHO: [[6 : 2 : 9], 3, [4]]
+ECHO: "--- vector / vector"
+ECHO: [3, 4]
+ECHO: [[], 3, 4]
+ECHO: [[2, 4], 3, 4]
+ECHO: [5, 6, "d", [3, 4]]
+ECHO: [[1, 0, 0], [2, 0, 0], 3, 0, 0]
+ECHO: [[1, 0, 0], [2, 0, 0], [3, 0, 0]]
+ECHO: [[1, 0, 0], [2, 0, 0], [3, 0, 0], [4, 4, 4], [5, 5, 5]]
+ECHO: "--- recursive function"
+ECHO: [[1, 1], [2, 4], [3, 9], [4, 16], [5, 25], [6, 36], [7, 49], [8, 64], [9, 81], [10, 100]]
contact: Jan Huwald // Impressum