Part of the
spatial-placement skill. The skill links here when your agent needs the detail.SKILL.md.
Shared rules
- Exact paths.
scenePathis an exactres://...tscn; node paths are relative to the scene root (./Facade/Wall_01orFacade/Wall_01). Editor selection is never used. - The scene must be open in the editor (any tab). A scene mutation with
scenePathopens it;summer_open_scenedoes too. A closed scene answersscene_not_open. - Physics needs the active tab. Only the active scene’s bodies are in the editor’s physics space. Ray queries on another tab fall back to visual AABBs and say so.
- Scene space. Points, normals and bounds in results are in the scene
root’s frame (world space in the editor).
position/start/endthat you pass for new instances are parent-local, likesummer_set_prop. - Evidence is named in every result:
visual_aabb: bounds of visibleGeometryInstance3Dnodes (the definitionsummer_align_distribute_3duses). Inspace: "local"the mesh boxes are projected onto the node’s own axes, so a rotated facade measures tightly. Never triangle contact.physics: a collider query (exact shapes, needs the active tab).mesh_triangles: computed from the mesh triangles themselves.markers: Marker3D anchors shipped with the kit.
- Compact results. Every result is under 5 KB. A list cut to fit is
declared in
truncated: {list: {shown, total}}. - No silent fallback. A missing op answers
engine_lacks_op; a measurement that cannot be made answers a namedfailure_reason. - Engine ops. Reads run one read-only GDScript probe
through
RunSceneScript(the op behindsummer_run_script: in-process, no child editor,undo: "none", no checkpoint). Mutations are ordinarySetProp,SnapToSurfaceandInstantiateSceneops: one undo step per request and one finalSaveScene. - Side effects of the probe. It blocks the editor for the few milliseconds
it runs, and the engine marks the active tab as unsaved after any
RunSceneScript, even a read-only one. Nothing in the scene changes.
Which tool answers which question
summer_inspect_asset
Measure an asset file without adding it to a scene. The probe loads the file,
instances it off the scene tree, measures, and frees it.
- Use when: before placing any kit piece you have not measured.
- Arguments:
path(res://.tscn.scn.glb.gltf.res.tres.mesh.obj),maxTriangles(100-300000, default 60000) for the face and loop analysis,detail(summary, the default, orfull). - Result:
\{ok, tool, detail, summary \{aabb, origin, plane_pairs, port_like_loops, triangles, mesh_count, anchor_count, collision_count, warnings?\}, frame: "asset_root", root_class, root_transform_identity, aabb \{min, max, size\}, origin \{fraction [x,y,z], label\}, mesh_count, meshes [\{path, tris, min, max, hidden?\}], triangles, planes [\{normal, offset, area, tris, cull_back, one_sided\}] (top 6 by area), open_loop_count, open_loops [\{id, index, mesh, center, direction, radius, vertices, max_dev, direction_ambiguous?\}], open_loops_listed?, open_loops_omitted?, open_chains, anchor_count, anchors [\{name, path, position, forward, up\}], collision_count, collision [\{path, shape, size|radius|height|faces|points, center, min, max, disabled?\}], analysis \{triangles_analyzed, triangle_budget, truncated, weld_m, plane_bin\}, evidence: "mesh_triangles"\}. - The summary block comes first and holds the facing evidence:
plane_pairs: the two largest pairs of opposite planes (a pair under 1% of the largest one’s area is left out), each{larger, opposite, separation}.oppositeisnullfor a single sheet. Every plane is\{axis?, normal, offset, area, one_sided\};axis(such as+z) is given only when the normal is within about 1 degree of it, so a baked yaw or a 45-degree corner face shows its exact normal with no label.separationis the distance between the two planes (the thickness; 0 for a sheet modelled with both faces).one_sided: truemeans the plane’s material culls back faces: it is invisible from behind its normal. A one-sided sheet with no opposite plane faces along its normal, and its back is the opposite axis.port_like_loops: ids of the open loops that look like pipe or duct ends: radius over 2 cm, a decided direction, and either a partner loop of similar radius facing more than 60 degrees away (the other end of a straight run or a bend) or, for an end piece with one opening (an outlet’s socket, a funnel’s mouth), the only loop opening on the bounding-box face it points through, with the piece reaching at least two radii behind it.warnings: says when the triangle budget cut the analysis and whichmaxTrianglescovers the whole mesh.
detail:summarylists inopen_loopsonly the port-like loops (with their ids and indices, whichsummer_connect_portsaccepts) and says how many it omitted.fulllists every loop, including the outline and holes of flat sheets.- Loop ids and order: loops are grouped by the piece axis their outward
direction is nearest (
+X,-X,+Y,-Y,+Z,-Z, then~X,~Y,~Zfor loops whose direction is undecided), the outermost along that axis first, then by centre x, y, z (in millimetres); the radius only breaks exact ties. Theidfollows that order:+Yis the outermost loop facing +Y,+Y#2the next one.indexis the position in the same order. Both are computed in the piece’s own frame, so they name the same loop on the placed node insummer_connect_ports, whatever its pose. A loop 30 degrees off +Y still groups under+Y: read itsdirection. - Reading it: the asset frame is the root’s own frame (its own transform
excluded), the frame
positionandrotation_degreesapply in.origin.labelsuch asx:center y:min z:centermeans bottom centre. Plane normals follow triangle winding, so they point out of the visible side. The largest planes with normals along one axis are usually the front and back: a wall with a large+zplane at the max z offset faces +z and its back is-z. The tool reports planes; you decide which is the front. Open loops are holes in the mesh: pipe and duct ends (directionpoints out of the piece), and also the open edge of single-sided geometry.max_devis how far the loop is from flat. - Limits: planes are binned at about 1.4 degrees and 1 cm. Vertices are
welded at 0.1 mm, so UV seams do not count as holes; double-sided or capped
ends have no loop. Hidden meshes are listed but not analysed.
one_sidedreadscull_modeof aBaseMaterial3D, orcull_disabled/cull_frontin a shader’s code; a mesh with no material counts as one-sided (the default material culls back faces). - Ops:
RunSceneScript(read-only probe).
summer_instantiate_scene (placed)
- Placement arguments:
position,rotation_degrees,scale([x, y, z], parent-local) ortransform("Transform3D(xx, xy, xz, yx, yy, yz, zx, zy, zz, ox, oy, oz)"). Nottransformwith the others; notscaleortransformwithtarget_size. - Behaviour:
InstantiateScene(its own request, as the engine requires), then oneSetProprequest on the node path the receipt reports (a name collision rename is followed), thenSaveScene. Without these fields it only instances the piece. - Result: the merged receipt (
resultsholds every op; the per-requestreceiptscopies are kept only when something failed) plusplacement {nodePath, applied, fields, space: "parent_local"}. - Ops:
InstantiateScene,SetProp,SaveScene.
summer_batch placement
InstantiateSceneops may carry the same placement fields (arrays or"Vector3(...)"strings). Each piece is one op; itsSetProps are sent right after it is created. Other ops are forwarded verbatim.receipt: "summary"returns\{ok, receipt, ops, requests, applied, failed, not_sent, saved, error?, failures [\{index, op, step?, failure_reason?, error\}], created_total, created [paths], renamed [\{index, requested, actual\}]\}.indexis the position in youropslist. Use it for every batch over a few ops: a full receipt for about 30 or more ops overflows the tool output limit.- Cost: the engine runs each
InstantiateSceneas its own request. The transforms of a run ofInstantiateSceneops are held back and sent together (up to 200SetProps per request) just before the next op that is not anInstantiateScene, riding along with it when it can. A batch of N placed pieces is therefore about N + 2 requests and N + 2 undo steps. Every later op in the list (aSetProp,SnapToSurface, a query, the save) runs after the transforms land. If anInstantiateScenefails, the pieces created before it still get their transforms; nothing is saved.
summer_place_adjacent
- Use when: facade modules edge to edge, a storey on the storey below, a cornice on a wall, a pier beside a bay.
- Arguments:
subject(moves),reference(stays),axis(x|y|z),side(max: subject on the reference’s +axis side;min: the -axis side),gap(default 0; negative overlaps),alignOtherAxes(min|center|max|nonefor both other axes, or per axis{y: "min", z: "max"}),space(world, orlocalfor the reference’s own axes). - Result:
\{subject, reference, evidence: "visual_aabb", space, moved, moved_by [world], position [parent-local], saved, reference_excludes_subject?, verify \{gap, residuals\}\}.verifyis a fresh read after the move. - Limits: bounds are visible-mesh AABBs; a piece with an overhang (a cornice lip) meets at its outermost point. If the subject is inside the reference, its geometry is left out of the reference’s bounds. A reference inside the subject is refused.
- Ops:
RunSceneScript(read, verify),SetProp(position),SaveScene.
summer_attach_to_surface
- Use when: mounting downpipes, gutters, lamps, AC units, signs, fire escapes on a wall (or anything on a ceiling or floor) with the right side against it.
-
Arguments:
subject;surface(node: the ray runs from the subject’s origin to the nearest point of the node’s bounds) and/orray \{origin, direction\}(with both, hits on other nodes are skipped);backAxis(the subject’s local axis that faces into the surface, default-z);upAxis(default+y);worldUp(default[0,1,0]);standoff(default 0);placeAt(current, the default, orhit);maxMove(default 2);maxDistance(ray length, default 20);collisionMask. -
Place the piece near its mount first (
summer_instantiate_scenewithposition): this tool turns it and pushes it onto the surface. -
Behaviour: the probe reads the piece’s visible bounds along its own axes
and casts the ray (physics first, visual AABB fallback declared). The piece is
turned so
backAxispoints along -normal andupAxisfollowsworldUpprojected onto the surface (on a floor or ceiling, where that is undefined, the current up is kept and a warning says so). The planned seat puts its back face (the extreme of its visible bounds alongbackAxis, not its origin) atstandoffin front of the plane through the hit point:placeAt: "current": the piece keeps its height and its place along the surface and moves only along the surface normal.placeAt: "hit": it also slides so the centre of its back face lands on the hit point (its height then follows the ray). OneSetPropturns it and puts its back face 5 cm in front of the planned seat;SnapToSurfacesweeps it along -normal (at moststandoff+ 0.3) and seats it atstandoffby its own evidence.
-
Checked before saving:
- a planned move longer than
maxMoveis refused before anything changes (move_exceeds_max_move,mutationApplied: false), and so is, withplaceAt: "current", a ray hit farther thanmaxMovealong the surface from the piece’s back face (hit_far_from_piece: aim the ray at the piece, or passplaceAt: "hit"to move it there); - with
surface, the seat must be on that node or inside it; with a ray only, a seat on another node is accepted only when it lies on the hit plane (a coplanar neighbour module, warnedseated_on_coplanar_<path>). Anything else is refused asseated_on_other_node; - a failed seat (
overlap_recovery_exceeded,gap_exceeds_hit_travel,surface_not_found,subject_not_ready) is reported withblockers \{evidence, overlapping, overlaps, first_contact \{path, distance\}\}, read where the seat started, and anext_step.
restored: true), saves nothing and answersok: falsewithseated_on,intended_surface,in_front_of_plan(how far in front of the planned seat it stopped) andnext_step. - a planned move longer than
-
Result:
\{subject, placed_at, seated_on, final_gap (the collider gap from SnapToSurface), back_face_gap (visible back face to the hit plane), moved_by, surface_hit \{evidence, path, point, normal, distance\}, orientation \{backAxis, upAxis, world_back, world_up\}, back_face_offset (origin to back face along backAxis), seat \{ok, evidence, supportPath, finalGap, gapErrorBound, initiallyOverlapping, backoffDistance, hitTravel, origin, warnings\}, standoff, saved, warnings\}. -
Warnings worth acting on:
visible_back_X_into_surface(the collider sits X behind the visible back face, so the mesh pokes into the wall: add X tostandoff);surface_not_flat_under_piece(it seated on the surface but in front of the hit plane);hit_normal_pointed_away_from_the_ray_flipped(the ray hit a back face; the piece still mounts on the ray’s side). -
Check after:
summer_starcastwithdirectionSpace: "local". Its local names follow the engine (forward= -Z,back= +Z), so a -Z back readsforwardblocked (or the surface incontactswhen seated flush) andbackopen. -
Limits: the back face is the visible-mesh AABB in the piece’s own axes;
the seat itself is collider-exact when the piece has colliders. A mesh-only
wall gives an AABB face normal (axis-aligned), and SnapToSurface’s own
evidence is then
visual_aabb. A mirrored (negative-scale) piece is refused. PassbackAxisfromsummer_inspect_asset, never a guess. -
Ops:
RunSceneScript(bounds and ray; the blockers read after a failed seat),SetProp(transform; the restore),SnapToSurface,SaveScene.
summer_repeat_along
- Use when: clamps every 0.45 m up a pipe, braces every 0.8 m along a duct, fence posts, bollards, a row of window modules.
- Arguments:
template(scene),parent,start; thenendwithspacing(as many as fit) orcount(spread start to end), ordirectionwithcountandspacing.align(start|center|end) places the leftover length withend+spacing.rotationDegrees,scale,namePrefix(copies are<prefix>_<n>, default the template file name). Coordinates are parent-local. At most 64 copies per call. - Result: the batch summary plus
\{template, parent, count, spacing, direction, first, last\}. - Ops:
InstantiateScene(one request per copy),SetProp(every copy’s transform in one request),SaveScene: N copies cost N + 2 requests.
summer_connect_ports
- Use when: pipe to pipe, duct to duct, gutter section to funnel or outlet, any two pieces with named anchors.
- Arguments:
subject(moves),subjectPort,target(stays),targetPort,gap(default 0),rollDegrees(default 0),maxTiltDegrees(0-180, default 5),allowTilt(default false),maxTriangles. A port is an open-loop id fromsummer_inspect_asseton that node’s scene (+Y,-Z#2; case does not matter,+Y#1is+Y), a Marker3D (or any Node3D) name under the node whose -Z axis points out of the port, or an open-loop index in the same stable order. A string shaped like an id is always read as an id. - Behaviour: the subject turns by the shortest rotation that makes its port
direction the opposite of the target’s, then by
rollDegreesabout the joined axis, then moves so the ports meet,gapapart along the target port direction. rollDegreesexactly:- axis: the target port’s direction reversed, pointing from the joint into
the target (
roll_axisin the receipt); - sign: right-hand rule about that axis; positive turns counter-clockwise when you look back along the axis from inside the target toward the subject;
- zero: the shortest turn from the subject’s current orientation, so the same
value gives different results from different start poses. Once the ports
are joined, a second call turns by exactly
rollDegreesabout the joint: 180 flips a bend’s free end to the other side.
- axis: the target port’s direction reversed, pointing from the joint into
the target (
- Tilt guard: the tilt is how far the join turns the subject’s up axis
(its local +Y); a turn about the up axis is no tilt. Over
maxTiltDegreesthe join is refused before anything changes:failure_reason: "tilt_exceeds_limit",tilt_degrees(predicted),max_tilt_degrees,subject_port,target_port,ports_within_limit [\{id|name, index, tilt_degrees, direction\}](the subject’s ports that would join within the limit, least tilt first) andsaved: false. PassallowTilt: truewhen the tilt is intended (a bend laid on its side, a sloped run); the receipt then warnstilt_allowed. - Result:
\{evidence (markers|mesh_triangles), subject_port, target_port, roll_axis, roll_degrees, rotated_degrees, tilt_degrees, max_tilt_degrees, moved_by, other_ports [\{kind, id|name, index?, position, direction, radius?, direction_ambiguous?\}], other_ports_total?, other_ports_predicted?, saved, verify \{distance, angle_degrees, tilt_degrees\}, warnings\}. A port that is not found answersport_not_foundwith the node’s loops by id (subject_portsortarget_ports). other_ports: every other port of the subject (its other Marker3D anchors for a marker port, its other open loops for an open-loop port, up to 8) with world position and outward direction after the move, measured by the verify read (other_ports_predicted: truewhen that read failed and they were computed from the move). Read where a bend’s free end now points instead of measuring it; if it points the wrong way, call again with therollDegreesthat turns it aboutroll_axis.- Limits: open-loop ports are only as exact as the mesh; an end whose
direction cannot be decided is flagged
direction_ambiguous. - Ops:
RunSceneScript(read, verify),SetProp(transform),SaveScene.
summer_raycast
- Use when: you need a surface and its normal at an arbitrary point before
anything is placed. (
summer_starcastcasts from an existing node.) - Arguments:
origin,direction,maxDistance(default 100),collisionMask,collideWithAreas,evidence(auto|physics|visual_aabb),exclude(up to 16 nodes). - Result:
\{hit, evidence, path, point, normal, distance, fallback?, fallback_reason?, nearer_visual_only?, origin, direction, physics_available, physics_unavailable_reason?, warnings\}. - Limits: visual AABB hits ignore boxes that contain the origin (counted in
warnings); their normal is a box face normal. - Ops:
RunSceneScript(read-only).
summer_measure
- Use when: checking a placement numerically: facade gaps, overlaps,
modules standing proud. (
summer_starcastreports clearance around one node in 26 directions; this compares named nodes.) - Arguments:
mode(pairwitha,b, optionalaxis;planewithnodes(2-32) andfacesuch as+z),space(world, orlocalfor the axes ofa/nodes[0]),tolerance(default 0.005). - Result, pair:
\{axes \{x|y|z: \{gap, relation: gap|touching|overlap, a, b, delta_min, delta_max, delta_center\}\}, boxes_overlap\};gap < 0is the overlap depth,delta_*is b minus a. - Result, plane:
\{coplanar, plane, spread, off_plane_count, nodes [\{path, face, deviation, off_plane: proud|recessed\}], no_geometry?\}. - Ops:
RunSceneScript(read-only).
Source:
library/skills/spatial-placement/references/kit-placement-tools.md, rendered from summer-engine@4.3.0 on npm.
