Repository navigation
Add a Smart Home example language - #50
Merged
Merged
Conversation
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.
This was referenced Oct 12, 2026
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.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Starter ID reuse, incomplete rule validation, and unsafe JSON interpolation cause correctness failures.
6 open findings
JSON-escape interpolated fixture values · New JSON-escape interpolated device configuration values · New Generate project-specific artifact ID remappings · New Run full spatial-rule validation during evolution · New Validate spatial rules after class-only updates · New Validate imported spatial rules before synchronization · New
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.
…ure/smart-home-example
…ample # Conflicts: # backend/src/__tests__/services/pipeline-engines.test.ts # docs/user-guide/spatial-rules.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


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.
Smart Home:HomecontainsRooms and devices.Hub,SmokeDetector,MotionSensor,Thermostat,Camera, andSmartPlugspecialize the abstractDevice(name,deviceId), so rules target every device through subtyping. Each device references itsroomand itshub;HubhasradioRangeMm.labelPosition, from Clip edges to node outlines and re-lay out the activity example #46; ignored without it).Home Layout:Floor Plandiagram (translucent rooms, devices, dashed links to the hub),Device Placement 3Ddiagram (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 aDevice Inventorytable. No GLB assets.Smart Home Device Configuration: writesdevice-configuration.json(deviceId, type, room, x/y/z in metres, yaw). Also shipped inexamples/codegen-projects/.resolvedReferencestarget 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 useresolvedReferences).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
src/(tests included) withoutfrontend/orexamples/.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 producesdevice-configuration.jsonin the browser with one device per line.