Skip to content

Add a Smart Home example language - #50

Merged
haphantran merged 11 commits into
mainfrom
feature/smart-home-example
Oct 13, 2026
Merged

haphantran merged 11 commits into
mainfrom
feature/smart-home-example

Conversation

@haphantran

@haphantran haphantran commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Depends on #45 (spatial rules); this branch contains its commits. Review the commits after #45's. The floor plan looks best with #46 merged too (room names inside the rooms, one-line device names, no link labels). Supersedes #47, which built a larger Smart Building example from an earlier version of the plan.

A third example language shows the spatial layer outside robotics, with containment rules and elevation policies used more heavily than in the warehouse.

  • Metamodel Smart Home: Home contains Rooms and devices. Hub, SmokeDetector, MotionSensor, Thermostat, Camera, and SmartPlug specialize the abstract Device (name, deviceId), so rules target every device through subtyping. Each device references its room and its hub; Hub has radioRangeMm.
  • Spatial rules: rooms do not overlap (plan); every device lies within a room (plan); smoke detectors at least 300 mm inside a room (plan); every device within 10,000 mm of a hub (volume). The values are illustrative, and the docs say so.
  • Model: five rooms (hallway, living room, kitchen, bedroom, bathroom), one hub, and 12 devices at their mounting heights; validates with zero issues. Room names are drawn inside the rooms (labelPosition, from Clip edges to node outlines and re-lay out the activity example #46; ignored without it).
  • Viewpoint Home Layout: Floor Plan diagram (translucent rooms, devices, dashed links to the hub), Device Placement 3D diagram (rooms are grounded thin floor slabs; devices are fallback boxes and cylinders with adjustable elevation: hub 900 mm, smoke detector 2,400, motion sensor 2,100, thermostat 1,500, camera 2,200, smart plug 300; range 0 to 3,000 mm), and a Device Inventory table. No GLB assets.
  • Generator Smart Home Device Configuration: writes device-configuration.json (deviceId, type, room, x/y/z in metres, yaw). Also shipped in examples/codegen-projects/.
  • Packaging: bundled with the other examples; the project dialog gains "Include Smart Home starter" and "Include UML Activity starter" switches. Every session is project-scoped and new projects start empty, so the activity example was otherwise unreachable.
  • Backend generation engine: exposes resolvedReferences target summaries (id, name, className) like the browser generator, so templates naming referenced elements render the same in pipeline runs (this also affects the existing Visual Components templates that use resolvedReferences).

Rules read the 3D pose. The floor plan is a scaled schematic (20 mm per pixel, north up), so as in the warehouse, violations are provoked in the 3D view or with the Position & Rotation fields; #45 now documents this.

Verification

  • Frontend: typecheck, lint, 398 tests pass. Backend: typecheck, lint, 697 tests pass (one run hit the known intermittent route-test failure from Add declarative spatial rules and yaw-aware spatial geometry #45; it passes alone and on rerun).
  • The backend Docker image builds: the backend tests read the example files at run time, because the image compiles src/ (tests included) without frontend/ or examples/.
  • Tests cover the metamodel and rules, the model (zero issues), a smoke detector moved onto the living-room wall (two rule errors), one 140 mm from the wall (margin error only) and one exactly 300 mm from it (no issue), a camera moved out of the house (outside every room and 11,585.5 mm from the hub), the elevation policies, generation in both the browser and backend engines (every device's x/y/z equals its modeled position in metres and its room matches its reference), the remapped starter bundle, and the starter imports.
  • Live, from the anonymized review snapshot of all open PRs merged together (docker compose up --build): the starter imports into a new project, the floor plan shows all rooms and devices with 0 validation issues, and the generator produces device-configuration.json in the browser with one device per line.

Spatial geometry now honours rotationZ. A shared oriented-box module
(shared/spatial-geometry.ts, mirrored in frontend/src/services/spatial)
builds each placed element's rotated footprint extruded over its
vertical interval. The JS/OCL spatial helpers keep their names and
signatures but use separating-axis overlap tests and exact convex-polygon
clearance, so rotated elements no longer get axis-aligned answers.

Metamodels gain spatialRules: noOverlap, minClearance, maxDistance,
within, and adjacentTo over placed instances of a source and a target
metaclass (subclasses included), in plan or volume mode. A pure
evaluator (shared/spatial-rules.ts plus the frontend mirror) runs in
browser validation, which marks both elements of an offending pair in
2D and 3D, and in the backend validation engine used by the headless
pipeline. A parity test keeps the two copies identical and checks that
they report the same issues.

Rules are persisted in a new metamodels.spatialRules column, validated
on create and update, carried through checkpoints and metamodel
evolution (a rule on a removed class blocks the evolution), removed with
a deleted class, and exported to Ecore as the EPackage annotation
spatialdsl/spatialRules. The metamodel editor gains a Spatial Rules
section with an add/edit dialog. The Smart Warehouse example declares a
FixedEquipment supertype and four rules that its bundled model
satisfies, and its unbounded controller references now use '*'.
The warehouse example said a violation could be provoked in the 2D view,
but 2D moves only change the 3D pose that rules check when an element's
2D and 3D plan positions coincide, which is not the case for the bundled
examples. Say so, and point to the 3D view and its Position & Rotation
fields instead.
A third example shows the spatial layer outside robotics. The Smart Home
metamodel has a Home with Rooms and an abstract Device specialized by
Hub, SmokeDetector, MotionSensor, Thermostat, Camera, and SmartPlug.
Every device has a deviceId and references its room and its hub; the hub
has a radio range. Its spatial rules keep rooms from overlapping, keep
every device inside a room (smoke detectors 300 mm from the walls), and
keep every device within 10 m of a hub in 3D. The bundled model (five
rooms, one hub, 12 devices) satisfies them.

The Home Layout viewpoint has a floor-plan diagram with dashed links to
the hub, a 3D diagram in which rooms are grounded floor slabs and
devices have adjustable mounting heights with per-class defaults, and a
device table. The Smart Home Device Configuration generator writes
device-configuration.json with each device's ID, type, room, position
in metres, and yaw.

New projects can start from the Smart Home or the UML Activity example
through two new switches in the project dialog, next to the Smart
Warehouse starter.

The headless generation engine now exposes resolvedReferences target
summaries (id, name, class) like the browser generator, so templates
that name referenced elements render the same in pipeline runs.
The backend Dockerfile compiles src/, tests included, with only backend/
and shared/ in the image. The spatial rules parity test imported the
frontend mirror and the warehouse fixtures statically, so the image build
failed. Load those files at run time instead, as the Sirius fixture test
does.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Starter ID reuse, incomplete rule validation, and unsafe JSON interpolation cause correctness failures.

6 open findings
What changed in this PR

Adds a Smart Home example language and starter while integrating declarative spatial rules across validation, persistence, interoperability, and generation.

Changes:

  • Adds Smart Home metamodel, model, views, generator, and project starter.
  • Adds rotation-aware spatial geometry and declarative rule evaluation.
  • Extends backend persistence, pipelines, Ecore/Sirius interop, documentation, and tests.
File Description
shared/​types/​index.ts Adds spatial-rule and issue types.
shared/​spatial-rules.ts Implements shared rule validation and evaluation.
shared/​spatial-geometry.ts Implements rotation-aware geometry.
README.md Advertises spatial rules.
frontend/​src/​services/​spatial/​spatial-rules.ts Adds frontend rule evaluator mirror.
frontend/​src/​services/​spatial/​spatial-geometry.ts Adds frontend geometry mirror.
frontend/​src/​services/​spatial/​index.ts Exports spatial modules.
frontend/​src/​services/​smart-warehouse-project-import.service.ts Generalizes starter imports.
frontend/​src/​services/​model/​model-validation.service.ts Runs spatial rules during validation.
frontend/​src/​services/​metamodel/​metamodel.service.ts Validates imported spatial rules.
frontend/​src/​services/​metamodel/​metamodel-api-sync.service.ts Synchronizes spatial rules.
frontend/​src/​services/​metamodel/​metaclass-crud.service.ts Removes rules for deleted classes.
frontend/​src/​services/​metamodel/​exampleData.service.ts Bundles Smart Home examples.
frontend/​src/​services/​metamodel/​ecore.service.ts Imports and exports rule annotations.
frontend/​src/​services/​diagram/​view-validation.service.ts Marks all related issue elements.
frontend/​src/​services/​constraint/​spatial.helpers.ts Makes constraint geometry rotation-aware.
frontend/​src/​models/​types.ts Adds frontend spatial-rule types.
frontend/​src/​examples/​data/​smart-warehouse-metamodel.json Adds warehouse spatial rules and superclass.
frontend/​src/​examples/​data/​smart-home-views.json Defines Smart Home saved views.
frontend/​src/​examples/​data/​smart-home-viewpoints.json Defines Smart Home representations.
frontend/​src/​examples/​data/​smart-home-project.json Defines the Smart Home generator.
frontend/​src/​examples/​data/​smart-home-model.json Adds the example home model.
frontend/​src/​examples/​data/​smart-home-metamodel.json Adds the Smart Home language.
frontend/​src/​components/​projects/​ProjectPickerPage.tsx Adds Smart Home and Activity starters.
frontend/​src/​components/​metamodel/​SpatialRulesEditor.tsx Adds spatial-rule authoring UI.
frontend/​src/​components/​metamodel/​components/​sidebar/​MetamodelSidebar.tsx Embeds the rule editor.
frontend/​src/​__tests__/​services/​view-validation.test.ts Tests related-element markers.
frontend/​src/​__tests__/​services/​spatial-rules.test.ts Tests rule semantics.
frontend/​src/​__tests__/​services/​spatial-helpers.test.ts Tests rotated geometry helpers.
frontend/​src/​__tests__/​services/​smart-warehouse-project-import.service.test.ts Tests new starter imports.
frontend/​src/​__tests__/​services/​smart-home-example.test.ts Tests the Smart Home bundle.
frontend/​src/​__tests__/​services/​ecore-xmi-interop.test.ts Tests rule annotation round trips.
frontend/​src/​__tests__/​services/​codegen-example-retarget.test.ts Updates generator retarget tests.
frontend/​src/​__tests__/​components/​SpatialRulesEditor.test.tsx Tests rule editor behavior.
examples/​codegen-projects/​smart-home-device-configuration-project.json Ships the Smart Home generator.
docs/​user-guide/​viewpoints.md Documents Smart Home views.
docs/​user-guide/​spatial-rules.md Adds the spatial-rules guide.
docs/​user-guide/​metamodels.md Documents rule authoring and Ecore.
docs/​user-guide/​code-generation.md Documents resolved references.
docs/​reference/​data-model.md Documents rule persistence.
docs/​reference/​api.md Documents rule API fields.
docs/​README.md Links spatial-rule documentation.
backend/​src/​services/​sirius-interop.service.ts Exports rules to Sirius Ecore.
backend/​src/​services/​pipeline/​model-validation.engine.ts Runs rules headlessly.
backend/​src/​services/​pipeline/​headless-pipeline.service.ts Maps persisted rules into pipelines.
backend/​src/​services/​pipeline/​code-generation.engine.ts Adds resolved reference summaries.
backend/​src/​services/​metamodel.service.ts Persists and validates rules.
backend/​src/​services/​lifecycle/​project-checkpoint.service.ts Checkpoints spatial rules.
backend/​src/​services/​lifecycle/​metamodel-evolution.service.ts Includes rules in evolution.
backend/​src/​__tests__/​services/​spatial-rules-parity.test.ts Tests frontend/backend parity.
backend/​src/​__tests__/​services/​sirius-interop.service.test.ts Tests Sirius rule export.
backend/​src/​__tests__/​services/​sharing.service.test.ts Updates metamodel fixtures.
backend/​src/​__tests__/​services/​project-checkpoint.service.test.ts Tests rule checkpoint compatibility.
backend/​src/​__tests__/​services/​pipeline-engines.test.ts Tests headless validation and generation.
backend/​src/​__tests__/​services/​metamodel.service.test.ts Tests rule persistence and validation.
backend/​src/​__tests__/​services/​metamodel-evolution.service.test.ts Tests evolution handling.
backend/​prisma/​schema.prisma Adds spatial-rule storage.
backend/​prisma/​migrations/​20261011000000_add_metamodel_spatial_rules/​migration.sql Creates the new JSONB column.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

"id": "c5500000-0000-4000-8000-000000000101",
"name": "Device Configuration List",
"language": "json",
"templateContent": "{\n \"home\": \"{{#each elementsByClassName.Home}}{{name}}{{/each}}\",\n \"units\": {\"position\": \"m\", \"yaw\": \"deg\"},\n \"devices\": [\n{{resetListComma \"devices\"}}{{#each elementsByClassName.Hub}}{{commaIfNeeded \"devices\"}}\n {\"deviceId\": \"{{deviceId}}\", \"type\": \"Hub\", \"room\": \"{{resolvedReferences.room.name}}\", \"x\": {{meters X}}, \"y\": {{meters Y}}, \"z\": {{meters Z}}, \"yaw\": {{#unless RZ}}0{{else}}{{RZ}}{{/unless}} }{{/each}}\n{{#each elementsByClassName.SmokeDetector}}{{commaIfNeeded \"devices\"}}\n {\"deviceId\": \"{{deviceId}}\", \"type\": \"SmokeDetector\", \"room\": \"{{resolvedReferences.room.name}}\", \"x\": {{meters X}}, \"y\": {{meters Y}}, \"z\": {{meters Z}}, \"yaw\": {{#unless RZ}}0{{else}}{{RZ}}{{/unless}} }{{/each}}\n{{#each elementsByClassName.MotionSensor}}{{commaIfNeeded \"devices\"}}\n {\"deviceId\": \"{{deviceId}}\", \"type\": \"MotionSensor\", \"room\": \"{{resolvedReferences.room.name}}\", \"x\": {{meters X}}, \"y\": {{meters Y}}, \"z\": {{meters Z}}, \"yaw\": {{#unless RZ}}0{{else}}{{RZ}}{{/unless}} }{{/each}}\n{{#each elementsByClassName.Thermostat}}{{commaIfNeeded \"devices\"}}\n {\"deviceId\": \"{{deviceId}}\", \"type\": \"Thermostat\", \"room\": \"{{resolvedReferences.room.name}}\", \"x\": {{meters X}}, \"y\": {{meters Y}}, \"z\": {{meters Z}}, \"yaw\": {{#unless RZ}}0{{else}}{{RZ}}{{/unless}} }{{/each}}\n{{#each elementsByClassName.Camera}}{{commaIfNeeded \"devices\"}}\n {\"deviceId\": \"{{deviceId}}\", \"type\": \"Camera\", \"room\": \"{{resolvedReferences.room.name}}\", \"x\": {{meters X}}, \"y\": {{meters Y}}, \"z\": {{meters Z}}, \"yaw\": {{#unless RZ}}0{{else}}{{RZ}}{{/unless}} }{{/each}}\n{{#each elementsByClassName.SmartPlug}}{{commaIfNeeded \"devices\"}}\n {\"deviceId\": \"{{deviceId}}\", \"type\": \"SmartPlug\", \"room\": \"{{resolvedReferences.room.name}}\", \"x\": {{meters X}}, \"y\": {{meters Y}}, \"z\": {{meters Z}}, \"yaw\": {{#unless RZ}}0{{else}}{{RZ}}{{/unless}} }{{/each}}\n ]\n}\n",
"id": "c5500000-0000-4000-8000-000000000101",
"name": "Device Configuration List",
"language": "json",
"templateContent": "{\n \"home\": \"{{#each elementsByClassName.Home}}{{name}}{{/each}}\",\n \"units\": {\"position\": \"m\", \"yaw\": \"deg\"},\n \"devices\": [\n{{resetListComma \"devices\"}}{{#each elementsByClassName.Hub}}{{commaIfNeeded \"devices\"}}\n {\"deviceId\": \"{{deviceId}}\", \"type\": \"Hub\", \"room\": \"{{resolvedReferences.room.name}}\", \"x\": {{meters X}}, \"y\": {{meters Y}}, \"z\": {{meters Z}}, \"yaw\": {{#unless RZ}}0{{else}}{{RZ}}{{/unless}} }{{/each}}\n{{#each elementsByClassName.SmokeDetector}}{{commaIfNeeded \"devices\"}}\n {\"deviceId\": \"{{deviceId}}\", \"type\": \"SmokeDetector\", \"room\": \"{{resolvedReferences.room.name}}\", \"x\": {{meters X}}, \"y\": {{meters Y}}, \"z\": {{meters Z}}, \"yaw\": {{#unless RZ}}0{{else}}{{RZ}}{{/unless}} }{{/each}}\n{{#each elementsByClassName.MotionSensor}}{{commaIfNeeded \"devices\"}}\n {\"deviceId\": \"{{deviceId}}\", \"type\": \"MotionSensor\", \"room\": \"{{resolvedReferences.room.name}}\", \"x\": {{meters X}}, \"y\": {{meters Y}}, \"z\": {{meters Z}}, \"yaw\": {{#unless RZ}}0{{else}}{{RZ}}{{/unless}} }{{/each}}\n{{#each elementsByClassName.Thermostat}}{{commaIfNeeded \"devices\"}}\n {\"deviceId\": \"{{deviceId}}\", \"type\": \"Thermostat\", \"room\": \"{{resolvedReferences.room.name}}\", \"x\": {{meters X}}, \"y\": {{meters Y}}, \"z\": {{meters Z}}, \"yaw\": {{#unless RZ}}0{{else}}{{RZ}}{{/unless}} }{{/each}}\n{{#each elementsByClassName.Camera}}{{commaIfNeeded \"devices\"}}\n {\"deviceId\": \"{{deviceId}}\", \"type\": \"Camera\", \"room\": \"{{resolvedReferences.room.name}}\", \"x\": {{meters X}}, \"y\": {{meters Y}}, \"z\": {{meters Z}}, \"yaw\": {{#unless RZ}}0{{else}}{{RZ}}{{/unless}} }{{/each}}\n{{#each elementsByClassName.SmartPlug}}{{commaIfNeeded \"devices\"}}\n {\"deviceId\": \"{{deviceId}}\", \"type\": \"SmartPlug\", \"room\": \"{{resolvedReferences.room.name}}\", \"x\": {{meters X}}, \"y\": {{meters Y}}, \"z\": {{meters Z}}, \"yaw\": {{#unless RZ}}0{{else}}{{RZ}}{{/unless}} }{{/each}}\n ]\n}\n",
return this.importBundleInto(projectId, exampleDataService.getActivityBundle());
}

private async importBundleInto(projectId: string, bundle: ExampleBundle): Promise<SmartWarehouseImportSummary> {
Comment on lines +378 to +382
asArray<SpatialRule>(next.spatialRules).forEach(rule => {
[rule?.sourceClassId, rule?.targetClassId].forEach(classId => {
if (typeof classId !== 'string' || nextClassNames.has(classId)) return;
const removedName = currentClassNames.get(classId);
blockers.push(removedName
Comment on lines 213 to +221
if (data.classes !== undefined) {
this.assertValidClassesVerticalPlacement(data.classes);
}
if (data.spatialRules !== undefined) {
const classes = data.classes !== undefined
? data.classes
: (await prisma.metamodel.findUnique({ where: { id }, select: { classes: true } }))?.classes;
this.assertValidSpatialRules(data.spatialRules, classes);
}
Comment on lines +154 to +165
rules.push({
id: uuidv4(),
name,
kind: parsed.kind,
sourceClassId,
targetClassId,
...(parsed.distanceMm !== undefined && { distanceMm: parsed.distanceMm }),
...(parsed.mode !== undefined && { mode: parsed.mode }),
severity: parsed.severity || 'error',
...(parsed.message !== undefined && { message: parsed.message }),
...(parsed.enabled !== undefined && { enabled: parsed.enabled }),
});
The backend image compiles src/, tests included, without frontend/ or
examples/, so the test reads the example files from disk instead of
importing them.
Room names are drawn inside the rooms (with #46), so smoke detectors and
the hub move off the room centres. Device icons stay inside their room
in the floor plan, hub links use the default arrow the 2D editor draws,
and the generated device list has one device per line.
Metamodel evolution now runs the full rule validation (kind, distance,
mode, severity, duplicate IDs) before a preview can be applied, a
classes-only update checks the stored rules against the new classes, and
Ecore import skips annotation rules that are not valid rules with a
warning instead of importing them.
The device configuration template renders the home name, device IDs, and
room names with a json helper, so quotes, backslashes, and newlines keep
the output valid JSON. The browser generator gains the json helper the
pipeline engine already had, and both render a missing value as null.

Starter imports now remap the example IDs once per project instead of
once per session: artifact IDs are global keys, so importing a starter
into a second project in the same session no longer reuses the first
project's IDs. Retrying an import into the same project still reuses
its IDs.
…ample

# Conflicts:
#	backend/src/__tests__/services/pipeline-engines.test.ts
#	docs/user-guide/spatial-rules.md
@haphantran
haphantran merged commit d87e21c into main Oct 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants