Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
a779326
feat: deliver selected Bundle 2 editor improvements
AxiomeCG Oct 10, 2026
fd3e4d4
Merge pascalorg/editor main (86f8e833) into feat/bundle-2-delivery-20…
AxiomeCG Oct 12, 2026
8c684e1
fix(core): port planPreviewSvg, which the MCP's import_plan_reference…
AxiomeCG Oct 12, 2026
c705c4a
fix(editor): Point and ask follows main's one building mode
AxiomeCG Oct 12, 2026
9b67efb
fix(mcp): create_roof is registered once, by the shared tools
AxiomeCG Oct 12, 2026
bf39e33
feat(roofs): a roof segment's fascia takes a height, a depth and its …
AxiomeCG Oct 12, 2026
332feb2
feat(nodes): register the facade and panel kinds the schema already c…
AxiomeCG Oct 12, 2026
cd704bb
test: bring the checks that name this branch's tools and behaviour up…
AxiomeCG Oct 12, 2026
0853582
fix(nodes, viewer): export subscribeWallRebuilds, import createMateri…
AxiomeCG Oct 12, 2026
e9a36f6
chore: facades are not part of bundle 2; remove the facade family
AxiomeCG Oct 12, 2026
7b0ed9d
test: the OpenAI annotation policy lists the tools the shared tools s…
AxiomeCG Oct 12, 2026
a5bed04
fix(core): export unitClientBrief from agent-operations
AxiomeCG Oct 12, 2026
86c9cb7
fix(core): validate plugin nodes and respect vertical collision bounds
AxiomeCG Oct 12, 2026
99ef572
refactor(core): make scene-tool evidence and batching optional
AxiomeCG Oct 12, 2026
1e9ba00
feat(core): a scene preview shows another state of the document witho…
AxiomeCG Oct 8, 2026
3969ee0
fix(editor): autosave stands still under a held scene preview
AxiomeCG Oct 8, 2026
11802d7
feat(editor): Point and ask's done card asks for more and shows the b…
AxiomeCG Oct 8, 2026
95ab7c4
fix(editor): the Point and ask card closes when the pointer is on nei…
AxiomeCG Oct 8, 2026
f9a863f
fix(editor): the Before tag clears the editor's own controls and Chan…
AxiomeCG Oct 8, 2026
ee65eca
fix(point-ask): scope room commits and retain Undo for writes
AxiomeCG Oct 8, 2026
f884e46
fix(point-ask): exclude foreign writes and resolve hosted frames
AxiomeCG Oct 8, 2026
35f3093
fix(point-ask): derive room suggestions from scoped contents
AxiomeCG Oct 8, 2026
c142c8b
fix: honor Point calm controls and initial hover dwell
AxiomeCG Oct 8, 2026
01f1bd1
fix(point-ask): hold to see before says why it cannot start, and take…
AxiomeCG Oct 12, 2026
bb33113
feat(point-ask): a host can say Pascal is working and retry an ask wi…
AxiomeCG Oct 12, 2026
6be9369
fix(audio): port control-aware construction landing cues
AxiomeCG Oct 12, 2026
ceb102a
fix: keep group and editor shortcuts out of editable controls
AxiomeCG Oct 12, 2026
b56cbb4
fix: preserve editable keys during group rotation
AxiomeCG Oct 12, 2026
4ae0888
fix: isolate remaining move and draft keyboard actions from fields
AxiomeCG Oct 12, 2026
8a6d04d
feat(core): port authored room and wall finish readback
AxiomeCG Oct 12, 2026
118f5ff
fix(core): keep wall-hosted furniture planar to its face
AxiomeCG Oct 12, 2026
2bec245
feat(core): report wall-bound furniture away from room walls
AxiomeCG Oct 12, 2026
5bc4756
fix(core): honor floor underlays and authored footprint bounds
AxiomeCG Oct 12, 2026
ec8fecf
fix(point-ask): a turn's changes go only to the pin of the ask that i…
AxiomeCG Oct 12, 2026
9edaab7
fix(point-ask): a pin that never heard its end is settled when the ch…
AxiomeCG Oct 12, 2026
93b8ef6
fix(editor): follow Pascal onto the target floor
AxiomeCG Oct 12, 2026
3f3f838
fix(editor): keep Pascal follow targets in sight
AxiomeCG Oct 12, 2026
bfac17a
test(editor): ensure manual camera moves pause Pascal follow
AxiomeCG Oct 12, 2026
08ee96e
fix(capture): an interior view waits for the scene to build and sets …
AxiomeCG Oct 12, 2026
f2e2e35
feat(point-ask): an ask sent while Pascal works is handed to the chat…
AxiomeCG Oct 12, 2026
8b3b227
fix(geometry-script): compile consecutive imports on one line
AxiomeCG Oct 12, 2026
9079960
fix: bind authored storey-height walls to the covering slab datum
AxiomeCG Oct 12, 2026
c245565
feat: report wall contacts with covering slabs in scene verification
AxiomeCG Oct 12, 2026
8eac146
docs: explain explicit storey-height walls and saved-scene correction
AxiomeCG Oct 12, 2026
b6c8b44
fix: top-anchor ceiling-mounted scripted objects
AxiomeCG Oct 12, 2026
7ffda44
feat: observe ceiling object bounds above attachments and in slabs
AxiomeCG Oct 12, 2026
933846f
docs: explain ceiling origins and explicit saved-object rebuilds
AxiomeCG Oct 12, 2026
3b9d3c4
fix(geometry-script): a mesh at a non-finite position fails the compi…
AxiomeCG Oct 12, 2026
8e76db8
fix(core): add_object refuses an object it cannot measure, before bui…
AxiomeCG Oct 12, 2026
9a1375c
fix(editor): the room pills' timers are called as functions, not as m…
AxiomeCG Oct 12, 2026
b6be116
fix(nodes): a door or window with clips draws its own leaves, not a s…
AxiomeCG Oct 12, 2026
fc28211
fix(editor): Follow Pascal puts back the cutaway, the floor and the h…
AxiomeCG Oct 12, 2026
f9ca67e
fix(editor): the walkthrough operates a door or window built from a s…
AxiomeCG Oct 12, 2026
6dbfbad
test: a scripted sliding door keeps its open clip through the compile…
AxiomeCG Oct 12, 2026
3ee769c
Merge Follow Pascal's view restore into the bundle 2 editor branch
AxiomeCG Oct 12, 2026
7762ba9
Merge crafter's door and window animation fixes into the bundle 2 edi…
AxiomeCG Oct 12, 2026
5417caf
Observe wall object depth conflicts and orient new back mounts
AxiomeCG Oct 12, 2026
05245db
Merge the wall object depth observations into the bundle 2 editor branch
AxiomeCG Oct 12, 2026
f9333f8
fix(editor): a scripted door that is open can be walked through in th…
AxiomeCG Oct 12, 2026
c9f5466
Merge the walk-through of an open scripted door into the bundle 2 edi…
AxiomeCG Oct 12, 2026
60653f1
Merge upstream pascalorg/editor main (80d7a2f2, Blender bpy modules #…
AxiomeCG Oct 12, 2026
6be2b6d
test: the refusals and the walk-through of a scripted door hold for a…
AxiomeCG Oct 12, 2026
857016e
fix: a roof outline's corner is bevelled past a mitre limit instead o…
AxiomeCG Oct 12, 2026
4cc6ee7
chore: photo location is not part of bundle 2; remove locate_photo
AxiomeCG Oct 12, 2026
60b7869
chore: the scene coherence checker is not part of bundle 2; remove it
AxiomeCG Oct 12, 2026
dc01533
chore: site works are not part of bundle 2; remove add_fence, add_sit…
AxiomeCG Oct 12, 2026
56b7b1b
chore: apartments, units and circulation tools are not part of bundle…
AxiomeCG Oct 12, 2026
bbb4e1b
chore: plan reading and reference construction are not part of bundle…
AxiomeCG Oct 12, 2026
95813f7
chore: request_upload is not part of bundle 2; remove its hints and t…
AxiomeCG Oct 12, 2026
da081b4
test: trim the animation-feel and copy-pinning tests both audits agre…
AxiomeCG Oct 12, 2026
00c76f0
chore: visual reference search is not part of bundle 2; remove its to…
AxiomeCG Oct 12, 2026
cad1839
chore: remove the modules this branch added that nothing imports
AxiomeCG Oct 12, 2026
2143eec
fix: a scripted item's plan footprint carries its asset's scale, rota…
AxiomeCG Oct 12, 2026
799e8f6
fix: a patch that loses a field removes it from the simulated scene, …
AxiomeCG Oct 12, 2026
232a5b1
fix: a scripted door, window or column that cannot be measured is ref…
AxiomeCG Oct 12, 2026
6e84468
fix: an unknown fascia finish is refused as an unknown roof finish is
AxiomeCG Oct 12, 2026
2ddbaf4
test: a wall-side item with no side is observed on the face the rende…
AxiomeCG Oct 12, 2026
6c46b85
Merge upstream pascalorg/editor main (5af1bef1: interior views, paint…
AxiomeCG Oct 13, 2026
ccf75a9
test: hold to see before is pinned for a turn that builds
AxiomeCG Oct 13, 2026
b2780f8
fix: a storey added and not built yet is not the roof level: the roof…
AxiomeCG Oct 13, 2026
97e173c
test: a batch on a host without a materials map refuses a native fini…
AxiomeCG Oct 13, 2026
fe9adab
feat(point-ask): the bubble says Queue behind messages that already w…
AxiomeCG Oct 13, 2026
03a8a02
fix: a roof over a storey looks past a roof level for a built storey …
AxiomeCG Oct 13, 2026
d7736a2
fix: a roof stands only on a roof level of its building, by one rule …
AxiomeCG Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ export function compiledSolid(
}
}

/**
* What a compile that could not measure the object leaves in the manifest: a mesh at a position
* that is not a number made every bound NaN. A structured clone (the chat's worker) keeps NaN; a
* JSON hop (a tab job answering the server) turns it to null.
*/
export function compiledUnmeasured(
hop: 'clone' | 'json',
mount: GeometryScriptMount = 'floor',
): CompiledGeometryScript {
const solid = compiledSolid([1.2, 2.8, 0.6], mount)
const nan: [number, number, number] = [Number.NaN, Number.NaN, Number.NaN]
const compiled = {
...solid,
params: { width: 7.7, display: true },
manifest: { ...solid.manifest, bounds: { min: nan, max: nan } },
}
return hop === 'json' ? JSON.parse(JSON.stringify(compiled)) : compiled
}

/** An object add_object built earlier, with the reason it was given. */
export function authoredItem(
id: string,
Expand Down Expand Up @@ -165,6 +184,46 @@ export const ADD_OBJECT_CASES: AddObjectCase[] = [
[1.2, 0.5, 0.4],
{ refusal: 'reason_required', mentions: ['reason'], beforeCompile: true },
),
// An agent's cabinet script put a material where a position goes, with one param on: the compile
// answered with NaN bounds and the item schema threw its raw validation array at the model.
{
name: 'an object whose bounds are not numbers is refused before its asset is built',
scene: floorScene,
input: {
code: CODE,
name: 'Library wall',
category: 'cabinet',
reason: 'Pascal has no library wall.',
params: { width: 7.7, display: true },
},
compiled: compiledUnmeasured('clone'),
expect: {
refusal: 'unmeasurable_object',
mentions: ['"Library wall" cannot be measured', '"display":true', 'nothing was added'],
},
},
{
name: 'bounds that crossed a JSON hop as null are refused the same way',
scene: floorScene,
input: {
code: CODE,
name: 'Library wall',
category: 'cabinet',
reason: 'Pascal has no library wall.',
},
compiled: compiledUnmeasured('json'),
expect: { refusal: 'unmeasurable_object', mentions: ['"Library wall" cannot be measured'] },
},
{
name: 'an edit whose rebuild cannot be measured is refused and leaves the object as it was',
scene: floorScene,
input: { nodeId: 'item_screen', code: CODE, params: { display: true } },
compiled: compiledUnmeasured('clone'),
expect: {
refusal: 'unmeasurable_object',
mentions: ['item_screen cannot be measured', 'nothing was changed'],
},
},
// Production saw whole houses of plain custom solids, nothing editable as walls or rooms.
create(
'a plain box with a wall’s size is refused: walls exist',
Expand Down Expand Up @@ -215,6 +274,46 @@ export const ADD_OBJECT_CASES: AddObjectCase[] = [
{ result: { mount: 'wall-side' }, node: { parentId: 'wall_south' } },
'wall-side',
),
create(
'a back-side wall mount defaults to facing away from its wall',
{ parentId: 'wall_south', side: 'back' },
[1, 1, 0.04],
{ result: { mount: 'wall-side' }, node: { side: 'back', rotation: [0, Math.PI, 0] } },
'wall-side',
),
create(
'a front-side wall mount defaults to facing away from its wall',
{ parentId: 'wall_south', side: 'front' },
[1, 1, 0.04],
{ result: { mount: 'wall-side' }, node: { side: 'front', rotation: [0, 0, 0] } },
'wall-side',
),
create(
'an explicit back-side wall mount rotation remains explicit',
{ parentId: 'wall_south', side: 'back', rotation: 0 },
[1, 1, 0.04],
{ result: { mount: 'wall-side' }, node: { side: 'back', rotation: [0, 0, 0] } },
'wall-side',
),
{
name: 'rebuilding a saved back-side wall object preserves its rotation',
scene: () => {
const scene = floorScene()
const previous = ItemNode.parse(scene.nodes.item_screen)
scene.nodes.item_screen = ItemNode.parse({
...previous, parentId: 'wall_south', wallId: 'wall_south', side: 'back',
asset: { ...previous.asset, attachTo: 'wall-side' },
})
const wall = WallNode.parse(scene.nodes.wall_south)
scene.nodes.wall_south = { ...wall, children: [previous.id] }
const level = LevelNode.parse(scene.nodes.level_0)
scene.nodes.level_0 = { ...level, children: [wall.id] }
return scene
},
input: { nodeId: 'item_screen', code: CODE },
compiled: compiledSolid([1, 1, 0.04], 'wall-side'),
expect: { result: { nodeId: 'item_screen' }, node: { rotation: [0, 0, 0], side: 'back' } },
},
create(
'a name naming a Pascal type is built, with a hint naming the tool for it',
{ name: 'Spiral staircase', category: 'stair' },
Expand Down
171 changes: 168 additions & 3 deletions packages/core/src/agent-operations/__fixtures__/build-room-cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ZoneNode,
} from '../../schema'
import { doorFacing } from '../../building/wall-openings'
import { reconcileSceneStructure } from '../../lib/structure-reconcile'
import { findBlockedDoors } from '../door-clearance'
import { findItemItemCollisions } from '../layout-clearance'
import type { AgentToolCase, SceneGraph } from './cases'
Expand Down Expand Up @@ -117,6 +118,34 @@ function roomsScene(): SceneGraph {
return graph(building, ground, upper, roof, wallG, door, window, kitchen, wallU)
}

/**
* Run 6's order: walls drawn first, one add_wall each, round CLEAR (and `extra` inside it), then the
* rooms the reconciler made of them, "Room 1" first; `renamed` is a name a person gave it since.
*/
function walledScene({ extra = [], renamed }: { extra?: [Pt, Pt][]; renamed?: string } = {}) {
return (): SceneGraph => {
const base = roomsScene().nodes as Record<string, AnyNode>
const walls = [...CLEAR.map((point, i): [Pt, Pt] => [point, CLEAR[(i + 1) % 4]!]), ...extra].map(
([start, end], i) =>
WallNode.parse({ id: `wall_c${i}`, parentId: 'level_g', start, end, height: 2.5 }),
)
const level = base.level_g as LevelNode
let nodes: Record<string, AnyNode> = {
...base,
level_g: { ...level, children: [...level.children, ...walls.map((wall) => wall.id)] },
...Object.fromEntries(walls.map((wall) => [wall.id, wall])),
}
const names: Record<string, string[]> = { zone: ['zone_walled', 'zone_side'] }
let count = 0
nodes = reconcileSceneStructure({
nodes,
mintId: (kind) => names[kind]?.shift() ?? `${kind}_walled${++count}`,
}).nodes as Record<string, AnyNode>
if (renamed) nodes.zone_walled = { ...nodes.zone_walled!, name: renamed } as AnyNode
return graph(...Object.values(nodes))
}
}

// ─── Checks the table cannot state: minted ids and derived construction ───────────────────────

const problems = (...entries: [boolean, string][]) =>
Expand Down Expand Up @@ -184,6 +213,24 @@ function roomBuilt(levelId: string, name: string, edges: number) {
}
}

/** The room zones of the ground floor inside CLEAR's box: the one per walled space there. */
const roomsInClear = (nodes: Nodes) =>
Object.values(nodes).filter(
(node) =>
node.type === 'zone' &&
node.parentId === 'level_g' &&
node.spaceRole === 'room' &&
node.polygon.every(([x, z]) => x >= -0.01 && x <= 4.01 && z >= 4.99 && z <= 8.01),
) as ZoneNode[]

const area = (polygon: readonly (readonly number[])[]) =>
Math.abs(
polygon.reduce((sum, [x, z], i) => {
const [nx, nz] = polygon[(i + 1) % polygon.length]!
return sum + x! * nz! - nx! * z!
}, 0) / 2,
)

const roomOn = (levelId: string) => (result: Record<string, unknown>, nodes: Nodes) =>
problems([
nodes[result.zoneId as string]?.parentId === levelId,
Expand Down Expand Up @@ -375,7 +422,7 @@ export const CREATE_ROOM_CASES: AgentToolCase[] = [
},
},
{
name: "a window sits at add_window's sill height; a door takes its size, hinge, swing and style",
name: 'a window is centred on its wall, as add_window places it; a door takes its size, hinge, swing and style',
tool: 'create_room',
scene: roomsScene,
input: room({
Expand All @@ -396,12 +443,17 @@ export const CREATE_ROOM_CASES: AgentToolCase[] = [
check: (result, nodes) => {
const window = nodes[(result.windowIds as string[])[0]!]
const door = nodes[(result.doorIds as string[])[0]!]
// Centred on the wall as it stands when the room is made, the storey's 2.8 m: the floor plate
// is derived afterwards, and a later add_window sees the slab (a window 2.5 cm lower).
const wall = window?.type === 'window' && window.wallId ? nodes[window.wallId] : undefined
const level = wall?.parentId ? nodes[wall.parentId] : undefined
const ceiling = level?.type === 'level' ? (level.height ?? Number.NaN) : Number.NaN
return problems(
[
window?.type === 'window' &&
near(window.position[1] - window.height / 2, 0.9) &&
near(window.position[1], ceiling / 2) &&
window.width === 1.5,
`window ${JSON.stringify(window)} is not 1.5 m wide on a 0.9 m sill`,
`window ${JSON.stringify(window)} is not 1.5 m wide and centred on its ${ceiling} m wall`,
],
[
door?.type === 'door' &&
Expand Down Expand Up @@ -488,6 +540,119 @@ export const CREATE_ROOM_CASES: AgentToolCase[] = [
},
expect: { refusal: 'outdoor_room_overlap', mentions: ['Kitchen'] },
},
// Run 6: 54 walls drawn first, then 14 create_room calls, each stacking its named zone on the
// "Room N" the walls had made: 16 twins.
{
name: 'a room over walls that already enclose it names the room they made, never a second zone',
tool: 'create_room',
scene: walledScene(),
input: room({ levelId: 'level_g' }),
expect: {
result: { ok: true, reusedWalls: 4, zoneId: 'zone_walled' },
after: { zone_walled: { name: 'Bedroom' } },
check: (result, nodes) => [
...roomBuilt('level_g', 'Bedroom', 4)(result, nodes),
...problems([
roomsInClear(nodes).length === 1,
`${roomsInClear(nodes).map((zone) => zone.name).join(', ')} stand over one walled room`,
]),
],
},
},
{
name: 'the walled room is named whichever way round and from whichever corner the polygon runs',
tool: 'create_room',
scene: walledScene(),
input: {
levelId: 'level_g',
name: 'Study',
polygon: [
[4, 8],
[4, 5],
[0, 5],
[0, 8],
],
},
expect: {
result: { ok: true, zoneId: 'zone_walled' },
check: (_, nodes) =>
problems([
roomsInClear(nodes).map((zone) => zone.name).join() === 'Study',
`${roomsInClear(nodes).map((zone) => zone.name).join(', ')} stand there, not Study alone`,
]),
},
},
{
name: 'a polygon over two walled spaces names the one holding most of it; the other keeps its name',
tool: 'create_room',
scene: walledScene({
extra: [
[
[3, 5],
[3, 8],
],
],
}),
input: room({ levelId: 'level_g', name: 'Master Bedroom' }),
expect: {
result: { ok: true },
check: (result, nodes) => {
const rooms = roomsInClear(nodes)
const master = nodes[result.zoneId as string] as ZoneNode | undefined
return problems(
[rooms.length === 2, `${rooms.length} rooms over two walled spaces`],
[
master?.name === 'Master Bedroom' && Math.abs(area(master.polygon) - 9) < 0.05,
`the named room is ${master?.name} over ${master && area(master.polygon)} m², not the 9 m² space`,
],
[
rooms.some((zone) => /^Room \d+$/.test(zone.name ?? '')),
'the smaller space lost its own name',
],
)
},
},
},
// The owner (7 October): a room a person named is never renamed silently, and never twinned: the
// refusal names it, and the agent either passes rename: true or asks the person.
{
name: 'a room a person named is refused with a choice, never twinned or renamed silently',
tool: 'create_room',
scene: walledScene({ renamed: 'Library' }),
input: room({ levelId: 'level_g' }),
expect: { refusal: 'room_named_by_person', mentions: ['Library', 'rename: true only if the person asked', 'ask them'] },
},
{
name: 'rename: true names the room a person named, in place, with no second zone',
tool: 'create_room',
scene: walledScene({ renamed: 'Library' }),
input: room({ levelId: 'level_g', rename: true }),
expect: {
result: { ok: true, zoneId: 'zone_walled' },
after: { zone_walled: { name: 'Bedroom' } },
check: (_, nodes) =>
problems([roomsInClear(nodes).length === 1, 'a second zone stands beside the renamed room']),
},
},
{
name: 'the same name over a room a person named has nothing to create',
tool: 'create_room',
scene: walledScene({ renamed: 'Bedroom' }),
input: room({ levelId: 'level_g' }),
expect: { refusal: 'room_named_by_person', mentions: ['already', 'Bedroom'] },
},
{
name: 'outdoor over a space walls already enclose names that room and drops its ceiling',
tool: 'create_room',
scene: walledScene(),
input: room({ levelId: 'level_g', name: 'Courtyard', outdoor: true }),
expect: {
result: { ok: true, zoneId: 'zone_walled', ceilingId: null },
after: { zone_walled: { name: 'Courtyard', hasCeiling: false } },
check: (_, nodes) =>
problems([roomsInClear(nodes).length === 1, 'the courtyard stands beside its room']),
},
},
{
name: 'a roof level takes no room',
tool: 'create_room',
Expand Down
Loading
Loading