Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
fdf90b3
fix(viewer): preserve imported lighting and matched scene captures
Aymericr Oct 17, 2026
3ddc4b9
fix(nodes): preserve authored glass materials on imported items
Aymericr Oct 18, 2026
97ed591
feat: let authored landscapes replace site ground presentation
Aymericr Oct 18, 2026
9752c53
fix: preserve retained roofs in level presentation
Aymericr Oct 18, 2026
585424f
fix: omit retained roofs from interior captures
Aymericr Oct 18, 2026
c63e9dd
fix: preserve physical finishes on native ceilings
Aymericr Oct 18, 2026
5d2df6d
feat: inspect Solo and Exploded views through scene tools
Aymericr Oct 18, 2026
2563c43
feat: preserve project lighting across editor and viewer
Aymericr Oct 18, 2026
261b407
fix: verify captured level presentation before returning images
Aymericr Oct 18, 2026
739ff66
fix: preserve authored directional shadows in project views
Aymericr Oct 18, 2026
14d45b7
fix: keep authored flat backgrounds in display color space
Aymericr Oct 18, 2026
3da8d6e
fix(viewer): preserve explicitly authored transparency
Aymericr Oct 18, 2026
7aaeb41
fix(viewer): expose scripted opening controls in standalone views
Aymericr Oct 18, 2026
9dde850
fix: classify retained ceiling shells for roof exploration
Aymericr Oct 19, 2026
61036ed
core: author_scene — a whole three.js design handed over as a payload…
wass08 Oct 16, 2026
0c5daea
mcp: author_scene — compile and store the items, write the conversion…
wass08 Oct 16, 2026
934ab39
core: verify_scene reads hosted items through their host, lets stacke…
wass08 Oct 17, 2026
ff17a77
skill + wiki: author_scene — convert a design written as three.js in …
wass08 Oct 17, 2026
4c76a4c
core: a preview colour for every textured material; author_scene's me…
wass08 Oct 17, 2026
8033010
core: author_scene is MCP-first, outside the shared contract list unt…
wass08 Oct 17, 2026
75abaca
core: author_scene salts a replacement's ids so a payload written ove…
wass08 Oct 17, 2026
a430fa8
core: author_scene after the acceptance runs — lights on the asset, a…
wass08 Oct 17, 2026
0123c8e
core: author_scene takes opening types, styles and modules, exact col…
wass08 Oct 19, 2026
c23e1da
mcp: author_scene compiles an opening's module and keeps hex colours …
wass08 Oct 19, 2026
dda3765
skill + wiki: author_scene's opening types and styles, face colours, …
wass08 Oct 19, 2026
3784dae
core: a slab lying on a wall's top is its cap, never its support
wass08 Oct 19, 2026
0e01f89
geometry-script: bake the back face of double- and back-sided materials
wass08 Oct 19, 2026
4f1b897
core: a new door's handle hangs opposite its hinges
wass08 Oct 19, 2026
c4fefb2
core: pin what a room-edge push keeps on a painted wall
wass08 Oct 19, 2026
64bb180
core: author_scene reads each wall's room side from the plan instead …
wass08 Oct 19, 2026
a2fba24
core: author_scene's replacement salt probes a level id, so the same …
wass08 Oct 19, 2026
a7b3710
core + mcp: author_scene keeps a wall item's distance from the face, …
wass08 Oct 19, 2026
dce1532
rooms: a floor finish built by a script for the room's floor, rebuilt…
wass08 Oct 19, 2026
8f58f32
core + mcp: author_scene writes a wall's skirting and a room's floor …
wass08 Oct 19, 2026
a81c101
walls: a face finish built by a script for exactly one face, rebuilt …
wass08 Oct 19, 2026
a3dbe7d
core + mcp: author_scene writes wall face scripts from a wall's faceL…
wass08 Oct 19, 2026
01578cd
geometry-script: Pascal cuts a floor or face script's build to its re…
wass08 Oct 19, 2026
db03714
geometry-script + author_scene: Pascal measures how much of a floor o…
wass08 Oct 19, 2026
f87c929
core + mcp: author_scene keeps authored glass, glow and surface on ex…
wass08 Oct 19, 2026
e52c6e1
core: add_object's floor-plate rule from main covers rugs, so the nam…
wass08 Oct 19, 2026
b601150
Merge author_scene with retained design and level exploration fixes
Aymericr Oct 19, 2026
82bca37
Preserve manual scene edits during atomic design conversion
Aymericr Oct 19, 2026
159690c
Classify conversion identities and expose contract defaults
Aymericr Oct 19, 2026
88d89c0
Recheck invocation liveness before committing scene conversion
Aymericr Oct 19, 2026
18a4ae6
Merge remote-tracking branch 'origin/main' into feat/v2-scene-integra…
Aymericr Oct 19, 2026
51d1ece
Bind conversion baselines to their owning building and test stopped c…
Aymericr Oct 19, 2026
effe912
Use the ESM CSG entry point in server-side geometry compilation
Aymericr Oct 19, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ export const NOTATION_CASES: NotationCase[] = [
* pixels, so neither is listed; a length or an angle added as a bare number fails the test.
*/
export const NOT_LENGTHS = new Set([
'author_scene.payload.levels[].lights[].intensity',
// Coordinates, in level metres.
'add_column.x',
'add_column.y',
Expand Down
115 changes: 105 additions & 10 deletions packages/core/src/agent-operations/author-scene.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { describe, expect, test } from 'bun:test'
import type { AuthorScenePayload } from '../agent-tools/author-scene'
import { authorScenePayload } from '../agent-tools/author-scene'
import { cloneNodesInto } from '../registry/subtree'
import type { AnyNode, CompiledGeometryScript, SceneMaterial } from '../schema'
import { CEILING_DRAW_OFFSET, resolveCeilingHeight } from '../services'
import {
applySceneChanges,
authorScene,
authorSceneCeilingPoints,
authorSceneFaceScripts,
Expand Down Expand Up @@ -177,6 +179,104 @@ const bedroom: AuthorScenePayload = authorScenePayload.parse({
})

describe('author_scene', () => {
test('a copied baseline cannot authorize replacement of another building', () => {
const first = run(bedroom, { bed, lamp: bed })
const saved = applySceneChanges(scaffold(), first.changes)
const copied = cloneNodesInto(
Object.values(saved).filter((node) => node.type !== 'site'),
{
rootId: 'building_1',
parentId: 'site_1',
},
)
const nodes = { ...saved, ...Object.fromEntries(copied.nodes.map((node) => [node.id, node])) }
expect(() =>
authorScene(
nodes,
{
payload: bedroom,
compiled: { bed, lamp: bed },
buildingId: copied.root.id,
replace: true,
},
{ activeLevelId: null },
),
).toThrow()
expect(nodes.building_1).toEqual(saved.building_1)
})
test('an existing building and unrelated buildings are never erased on a first conversion', () => {
const scene = scaffold()
const first = run(bedroom, { bed, lamp: bed })
const authored = applySceneChanges(scene, first.changes)
const foreign = Object.fromEntries(
Object.entries(authored).map(([id, node]) => [
`other_${id}`,
{
...node,
id: `other_${id}`,
parentId: node.parentId ? `other_${node.parentId}` : null,
...('children' in node ? { children: node.children.map((id) => `other_${id}`) } : {}),
},
]),
) as Record<string, AnyNode>
const combined = { ...scene, ...foreign }
const outcome = authorScene(
combined,
{ payload: bedroom, compiled: { bed, lamp: bed } },
{ activeLevelId: 'level_0' },
)
const saved = applySceneChanges(combined, outcome.changes)
for (const [id, node] of Object.entries(foreign)) expect(saved[id]).toEqual(node)
const manual = { ...authored, building_1: { ...authored.building_1!, metadata: {} } }
expect(() =>
authorScene(
manual,
{ payload: bedroom, compiled: { bed, lamp: bed }, replace: true },
{ activeLevelId: null },
),
).toThrow()
})

test('regeneration keeps a manual move, an added object and a deleted object, and refuses an overlapping move', () => {
const first = run(bedroom, { bed, lamp: bed })
const saved = applySceneChanges(scaffold(), first.changes)
const bedId = first.result.ids.bed!
const lampId = first.result.ids.lamp!
const moved = { ...saved[bedId]!, position: [0.4, 0, -1] } as AnyNode
const manual = applySceneChanges(saved, {
update: [{ id: bedId, data: moved }],
delete: [lampId],
create: [
{
node: {
...saved[bedId],
id: 'item_manual',
children: [],
parentId: first.result.ids['level/0'],
} as AnyNode,
},
],
})
const repeat = authorScene(
manual,
{ payload: bedroom, compiled: { bed, lamp: bed }, replace: true },
{ activeLevelId: null },
)
const after = applySceneChanges(manual, repeat.changes)
expect(after[bedId]).toEqual(manual[bedId])
expect(after.item_manual).toEqual(manual.item_manual)
expect(after[lampId]).toBeUndefined()
const changed = structuredClone(bedroom)
changed.levels[0]!.items![0]!.position = [0.8, 0, -1]
expect(() =>
authorScene(
manual,
{ payload: changed, compiled: { bed, lamp: bed }, replace: true },
{ activeLevelId: null },
),
).toThrow()
})

test('writes the design as native nodes and reports what it did', () => {
const outcome = authorScene(
scaffold(),
Expand Down Expand Up @@ -375,9 +475,7 @@ describe('author_scene', () => {
{ payload: bedroom, compiled: { bed, lamp: bed } },
{ activeLevelId: 'level_0' },
)
const scene = scaffold()
delete scene.level_0
for (const { node } of first.changes!.create!) scene[node.id] = node
const scene = applySceneChanges(scaffold(), first.changes)
expect(() =>
authorScene(
scene,
Expand All @@ -391,13 +489,10 @@ describe('author_scene', () => {
{ activeLevelId: null },
)
expect(again.result.walls).toBe(4)
// Written over itself, the same payload gets fresh ids, so nothing is created and then deleted.
const firstIds = new Set(first.changes!.create!.map((c) => c.node.id))
expect(again.changes!.create!.length).toBe(first.changes!.create!.length)
expect(again.changes!.create!.some((c) => firstIds.has(c.node.id))).toBe(false)
expect(again.changes!.delete).toEqual([
first.changes!.create!.find((c) => c.node.type === 'level')!.node.id,
])
expect(again.result.ids).toEqual(first.result.ids)
expect(again.changes!.create).toEqual([])
expect(again.changes!.delete).toEqual([])
expect(applySceneChanges(scene, again.changes)).toEqual(scene)
})

test('an opening takes its type, style, leaves and a handle opposite the hinge', () => {
Expand Down
Loading
Loading