Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fd2cb38
Fix project url
argenos Jul 7, 2025
eeb76cf
Refactor get_coord_value for simplicity
argenos Jul 17, 2025
d62bf3f
Refactor querying values from lists
argenos Jul 17, 2025
40b74c5
Handle coordinates property in transformations
argenos Jul 17, 2025
37f6ba7
Refactor coordinates queries for reuse
argenos Jul 17, 2025
698fd17
Remove duplicated code from transf. matrix function
argenos Jul 17, 2025
21f492d
Rename waypoint coord to clarify wrt world frame
argenos Jul 17, 2025
7bae033
Return floats in point position
argenos Jul 22, 2025
1ed3385
Fixed transformation of points
argenos Jul 22, 2025
a1246ca
Use prefix from frame for goal frame URI
argenos Jul 22, 2025
e645616
Fix imports add g typing
argenos Aug 12, 2025
bcbc251
Add new door concepts to 3D mesh generation
argenos Aug 12, 2025
5957744
Add TTS wall description generation
argenos Aug 12, 2025
66c0419
Add gltf export
argenos May 6, 2025
88fec96
Fix args in occ grid generation
argenos Sep 11, 2025
8d99f52
Add unit conversion for length measurements in queries
argenos Sep 11, 2025
004f760
Add occ grid generation from IFC files
argenos Sep 11, 2025
69f6c90
Add CLI options to exclude doors and specify format in mesh gen
argenos Sep 11, 2025
e006da8
Add visualization of task elements to occ grid
argenos Nov 21, 2025
43e2d18
WIP: Milling task description
argenos Nov 21, 2025
db471f3
Add initial version of ifc2fpm transformation
argenos Nov 21, 2025
c5ac2ad
Add polyhedron faces to rectangle profile
argenos Nov 24, 2025
1444d6d
Add basic logging
argenos Nov 25, 2025
8a44fb1
Validate yaml files from jinja templates
argenos Dec 5, 2025
6991e9f
Add space boundary information to fpm transformation
argenos Dec 5, 2025
7c44cb3
Add ducts for milling task from IFC
argenos Dec 5, 2025
b133f95
Add missing type
argenos Dec 5, 2025
846ac4c
Update .gitignore
argenos Dec 5, 2025
43b210d
Reorganize args for CLI tts command
argenos Dec 8, 2025
a56b940
Add ifcld as a dependency
argenos Dec 8, 2025
4e33907
Remove duplicated code in mapped item
argenos Dec 8, 2025
d4f8a57
Handle doors with extruded area solid
argenos Dec 9, 2025
c06f78f
Refactor duplicated code
argenos Dec 9, 2025
107fad8
Handle door shapes composed of multiple items
argenos Dec 9, 2025
17bef9f
Return empty list if RDF.nil
argenos Dec 9, 2025
f4f0dfa
Fix overwriting variable in for loop
argenos Dec 9, 2025
d559910
Get units from IFC model
argenos Dec 10, 2025
51866c8
Handle multiple mapped items
argenos Dec 11, 2025
1b246ff
Add visualize arg to occ-grid
argenos Dec 11, 2025
14e91b0
Add command to visualize frame tree
argenos Dec 11, 2025
6749c23
Fix handling of units with prefixes
argenos Dec 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/


output/*
output/*
*.bkp

docs/_site/
5 changes: 3 additions & 2 deletions docs/_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ title: Scenery Builder
description: Model-to-model transformation of floorplan models to execution artefacts (including ROS simulation)
theme: just-the-docs

url: https://secorolab.github.io/scenery_builder

baseurl: "/scenery_builder" # the subpath of your site, e.g. /blog
url: https://secorolab.github.io
repository: secorolab/scenery_builder # for github-metadata

permalink: pretty
defaults:
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ dependencies = [
"rdflib@git+https://github.com/secorolab/rdflib@fix/multi-type-scoped-context",
"click>=8.1.7",
"pyyaml",
"floorplan_dsl@git+https://github.com/secorolab/FloorPlan-DSL"
"floorplan_dsl@git+https://github.com/secorolab/FloorPlan-DSL",
"transforms3d",
"ifcld@git+https://github.com/secorolab/ifcld"
]
requires-python = ">= 3.11"
readme = "README.md"
Expand Down
41 changes: 41 additions & 0 deletions src/fpm/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import logging


class AnsiColorFormatter(logging.Formatter):
def format(self, record: logging.LogRecord):
no_style = "\033[0m"
bold = "\033[91m"
grey = "\033[90m"
yellow = "\033[93m"
red = "\033[31m"
red_light = "\033[91m"
blue = "\033[34m"
start_style = {
"DEBUG": grey,
"INFO": no_style,
"WARNING": yellow,
"ERROR": red_light,
"CRITICAL": red + bold,
}.get(record.levelname, no_style)
end_style = no_style
return f"{start_style}{super().format(record)}{end_style}"


logger = logging.getLogger("floorplan")
logger.setLevel(logging.INFO)
fh = logging.FileHandler("floorplan-cli.log")
fh.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
file_formatter = logging.Formatter(
"{asctime} | {levelname:<8s} | {name:<30s} | {message}",
style="{",
)
console_formatter = AnsiColorFormatter(
"{asctime} | {levelname:<8s} | {name:<30s} | {message}",
style="{",
)
# ch.setFormatter(console_formatter)
fh.setFormatter(file_formatter)
logger.addHandler(ch)
logger.addHandler(fh)
172 changes: 164 additions & 8 deletions src/fpm/cli.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import os
import click
import logging

from fpm.generators.dot import visualize_frame_tree
from fpm.graph import build_graph_from_directory, get_floorplan_model_name
from fpm.generators.gazebo import gazebo_world, door_object_models
from fpm.generators.tasks import get_disinfection_tasks
from fpm.generators.occ_grid import get_occ_grid
from fpm.generators.mesh import get_3d_mesh
from fpm.generators.polyline import get_polyline_floorplan
from fpm.generators.door_keyframes import get_keyframes
from fpm.generators.tts import (
gen_tts_wall_description,
gen_tts_task_description,
gen_ros_frames,
)
from fpm.generators.scenery import generate_fpm_rep_from_rdf
from textx import generator_for_language_target, metamodel_for_language

logger = logging.getLogger("floorplan.cli")
logger.setLevel(logging.DEBUG)


def configure(ctx, param, filename):
if not filename:
return
import tomllib

logger.debug("Using config file: %s", filename)

ctx.default_map = dict()
with open(filename, "rb") as f:
data = tomllib.load(f)
Expand Down Expand Up @@ -117,14 +130,15 @@ def transform(ctx, model_path, output_path, **kwargs):

This requires that the [FloorPlan DSL](https://github.com/secorolab/FloorPlan-DSL) is installed.
"""
print(model_path, output_path)
logger.debug("transform command arguments: %s", kwargs)
logger.debug("%s %s", model_path, output_path)
generator = generator_for_language_target("fpm", "json-ld")
mm = metamodel_for_language("fpm")
model = mm.model_from_file(model_path)
try:
generator(mm, model, output_path, overwrite=True)
except Exception as e:
print(f"Error transforming model: {e}")
logger.error(f"Error transforming model: {e}")


@floorplan.command(short_help="Generate FPM variations from a variation model")
Expand Down Expand Up @@ -181,10 +195,10 @@ def variation(ctx, model_path, variations, seed, output_path, **kwargs):
See https://github.com/secorolab/FloorPlan-DSL/blob/devel/docs/tutorials/variation.md
for more information on creating variation models.
"""
print(f"Generating {variations} variation(s) from {model_path}")
print(f"Output path: {output_path}")
logger.info(f"Generating {variations} variation(s) from {model_path}")
logger.info(f"Output path: {output_path}")
if seed is not None:
print(f"Using seed: {seed}")
logger.info(f"Using seed: {seed}")

generator = generator_for_language_target("fpm-variation", "fpm")
mm = metamodel_for_language("fpm-variation")
Expand All @@ -200,7 +214,31 @@ def variation(ctx, model_path, variations, seed, output_path, **kwargs):
seed=seed,
)
except Exception as e:
print(f"Error generating variations: {e}")
logger.error(f"Error generating variations: {e}")


@floorplan.command(
short_help="Generate FPM JSON-LD models from an IFCLD model",
)
@click.pass_context
@click.option(
"-m",
"--model",
"model_path",
type=click.Path(exists=True, resolve_path=True, file_okay=True, dir_okay=False),
required=True,
help="Path to the fpm model to transform into JSON-LD",
)
@click.option(
"-o",
"--output-path",
type=click.Path(exists=True, resolve_path=True),
default=os.path.join("."),
help="Output path for generated artefacts",
)
def ifc(ctx, model_path, output_path, **kwargs):

generate_fpm_rep_from_rdf(model_path, output_path)


@floorplan.group(
Expand Down Expand Up @@ -245,9 +283,13 @@ def variation(ctx, model_path, variations, seed, output_path, **kwargs):
def generate(ctx, inputs, **kwargs):
"""Generate execution artefacts from JSON-LD models"""

print(kwargs)
logger.debug("generate command arguments: inputs: %s, kwargs: %s", inputs, kwargs)
_load_graph_to_ctx(ctx, inputs)


g = build_graph_from_directory(inputs)
def _load_graph_to_ctx(ctx, input_paths):
logger.debug("Loading graph from paths: %s", input_paths)
g = build_graph_from_directory(input_paths)
try:
model_name = get_floorplan_model_name(g)
except ValueError as e:
Expand All @@ -260,6 +302,18 @@ def generate(ctx, inputs, **kwargs):

@generate.command(short_help="Generate a 3D-mesh of the floorplan")
@click.pass_context
@click.option(
"--include-doors",
is_flag=True,
help="Flag to indicate that the mesh should include the door meshes",
)
@click.option(
"--format",
type=click.Choice(["stl", "gltf"], case_sensitive=False),
default="stl",
show_default=True,
help="Output format of the 3D mesh",
)
def mesh(ctx, **kwargs):
"""Generate a 3D-mesh in STL or gltF 2.0 format"""
get_3d_mesh(**ctx.obj, **ctx.parent.params, **kwargs)
Expand Down Expand Up @@ -412,8 +466,25 @@ def gazebo(ctx, **kwargs):
show_default=True,
help="Value for cells to be considered free in the occupancy map",
)
@click.option(
"--source",
type=click.Choice(["fpm", "bim"], case_sensitive=False),
default="fpm",
show_default=True,
help="ROS version for launch files",
)
@click.option(
"--visualize-frames",
type=click.Choice(
["wall", "door", "entryway", "space", "opening"], case_sensitive=False
),
help="Which element frames to visualize",
multiple=True,
)
def occ_grid(ctx, **kwargs):
"""Generate the occupancy grid map of the floorplan"""
logger.info("Generating occupancy grid...")
logger.debug("Arguments: %s", kwargs)
get_occ_grid(**ctx.obj, **ctx.parent.params, **kwargs)


Expand Down Expand Up @@ -469,6 +540,91 @@ def door_keyframes(ctx, **kwargs):
get_keyframes(**ctx.obj, **ctx.parent.params, **kwargs)


@generate.command()
@click.pass_context
@click.option(
"--robot-translation-x",
"x",
type=click.FLOAT,
default=-1.0,
show_default=True,
help="Translation of the robot in x wrt to a task element for a navigation goal",
)
@click.option(
"--robot-translation-z",
"z",
type=click.FLOAT,
default=1.7,
show_default=True,
help="Translation of the robot in z wrt to a task element for a navigation goal",
)
@click.option(
"--ros-frames",
is_flag=True,
help="Generate a ROS launch file with frame transformations for task elements",
)
@click.option(
"--visualize",
is_flag=True,
help="Generate images visualizing the environment and tasks",
)
def tts(ctx, **kwargs):
"""Generate the artefacts for the TTS simulator"""
logger.info("Generating artefacts for the TTS simulator...")
logger.debug("Arguments: %s", kwargs)
gen_tts_wall_description(**ctx.obj, **ctx.parent.params, **kwargs)
outlets, ducts = gen_tts_task_description(**ctx.obj, **ctx.parent.params, **kwargs)
if kwargs.get("ros_frames"):
gen_ros_frames(**ctx.obj, **ctx.parent.params, **kwargs)
if kwargs.get("visualize"):
logger.info("Visualizing milling task for the outlets on occupancy grid")
get_occ_grid(
**ctx.obj, **ctx.parent.params, **kwargs, outlets=outlets, source="bim"
)
get_occ_grid(
**ctx.obj, **ctx.parent.params, **kwargs, ducts=ducts, source="bim"
)
logger.info("Visualizing frames on occupancy grid")
get_occ_grid(
**ctx.obj,
**ctx.parent.params,
**kwargs,
visualize_frames=["outlet", "duct", "wall"],
source="bim",
)


@floorplan.command(short_help="Visualize aspects of a floorplan model")
@click.pass_context
@click.option(
"-i",
"--inputs",
"--input-path",
type=click.Path(exists=True, resolve_path=True),
required=True,
multiple=True,
help="Path with JSON-LD models to be used as inputs",
)
@click.option(
"-o",
"--output-path",
type=click.Path(exists=True, resolve_path=True),
default=os.path.join("."),
help="Output path for generated artefacts",
)
def visualize(ctx, inputs, output_path, **kwargs):
floorplan_elements = ["Space", "Opening", "Wall", "Door", "Entryway", "DoorPanel"]
print(ctx.obj, ctx.parent.params)
_load_graph_to_ctx(ctx, inputs)
visualize_frame_tree(
output_path=output_path,
floorplan_elements=floorplan_elements,
**ctx.obj,
# **ctx.parent.params,
**kwargs,
)


if __name__ == "__main__":
import sys

Expand Down
26 changes: 26 additions & 0 deletions src/fpm/generators/dot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import subprocess

from fpm.graph import get_floorplan_elements, get_frame_tree
from fpm.utils import load_template, save_file


def visualize_frame_tree(g, floorplan_elements, **kwargs):
import os

output_path = kwargs.get("output_path")
print(kwargs)
element_poses = get_floorplan_elements(g, floorplan_elements)
frames = get_frame_tree(g, element_poses)

template = load_template("frame-tree.dot.jinja")

file_name = "frame-tree"
frames_gv = "{:s}.gv".format(file_name)
frames_pdf = "{:s}.pdf".format(file_name)
contents = template.render(frames=frames)
save_file(output_path, frames_gv, contents)

dot_file_path = os.path.join(output_path, frames_gv)
pdf_file_path = os.path.join(output_path, frames_pdf)
cmd = ["dot", "-Tpdf", dot_file_path, "-o", pdf_file_path]
subprocess.Popen(cmd).communicate()
Loading