Diagonals: streets, walls, fences, hedges and house walls at any angle
Deploy web Studio / deploy (push) Successful in 1m5s

Object frames turn by any angle (quarter turns stay exact, so scenes along
the axes render byte for byte as before); a box in a turned frame is a
prism with its own corners. Rotation is kept as given (normalised to
0..360), no longer snapped to quarter turns.

Depth sort: sprites whose plan outline is not their box carry their convex
outline; when boxes overlap, an outline edge that separates two sprites
orders them. A long diagonal wall sorts right against both sides.

Joints (line_meet, end_cut, cut_box in util.rhai): where a piece off the
axes is involved, corners are mitred and T-junctions slanted; fences put a
corner post or pillar in its own sprite and mitre stone bases; Y joints let
the lowest id run through. Streets: every end part is a cut e + k*w, so
side streets come in at any angle (slanted sidewalk gaps), two streets meet
in a mitre with seam patches; fixes a curve's outer sidewalk that was
skipped when its kerb and back lines had different point counts.

Studio: runs and drags keep to x/y or snap to 45 degree diagonals, Ctrl
gives any angle; a road run turning 45 degrees gets a 45 degree curve;
Rotation is a number box. Streets demo: a diagonal lane with a hedge.
MCP guidance, SCRIPTING/README/ROADMAP.

Tests: Rust 76, Studio and web checks green.
This commit is contained in:
grabowski committed 2026-10-25 13:22:59 +07:00
1 parent c91d63663b
commit 96d472625f
18 files changed
+1051 -178

No files matched your search

+8 -5
View File
@@ -14,7 +14,8 @@ pub const PROTOCOL: &str = "2025-06-18";
const INSTRUCTIONS: &str = "isokit edits an isometric scene (a site plan drawn as a SimCity-style 3D view). \
World coordinates are metres: x east, y north, z up. The scene is a list of objects; each has a type (a prefab), \
a position (x, y = centre of its footprint), a rotation in 90 degree steps and props from the prefab's schema. \
a position (x, y = centre of its footprint), a rotation in degrees (any angle, counter-clockwise from east; \
90 degree steps keep things along the axes) and props from the prefab's schema. \
Start with project_info and list_prefabs, then add_object / update_object. Use render_view (summary) to see where \
everything ended up and check_project to find problems. Edits are saved at once and show live in an open Studio. \
Houses are built from parts (categories House and Furniture): a floor per room (w x d), a house_wall along each edge \
@@ -23,7 +24,9 @@ then windows and doors with x, y on a wall line (they sit on that wall and cut t
the wall's +y side, rot + 180 swings it the other way), a roof (w x d = outer size of the walls) and furniture \
(fronts face local -y, backs go against a wall). Level gf shows the house cut open; a part's storey is its z / 3 m. \
Fences (props.style: mesh, wood, bars, half stone, cobble, brick) and site walls join where their centre-line ends \
meet, so for a plot boundary put each piece's ends exactly on the next piece's end or line. \
meet, so for a plot boundary put each piece's ends exactly on the next piece's end or line. Line pieces (roads, walls, \
fences, hedges, house walls) can run at any angle: a piece from (x0, y0) to (x1, y1) has x, y = the midpoint, \
w = the distance and rot = atan2(y1 - y0, x1 - x0) in degrees; where diagonal pieces meet, corners are mitred. \
Production lines (category Industry): a producer (props.rate per minute, product) puts goods out on its local +x \
side; a conveyor (rot 0 runs towards +x, 90 +y, 180 -x, 270 -y; w = length, props.speed m/s) starting exactly \
at that side carries them, and a conveyor whose start is at another's end carries on with them (corners join). \
@@ -32,8 +35,8 @@ A robot_arm (props.reach, default 2 m) picks goods off the end of the nearest co
them on the start of another conveyor within reach, or on a pallet beside it; place it 1.5 to 2 m past a belt's end. \
Streets (category Ground) join by their centre lines: a road (x, y = middle, rot 0 along x, w = length, d = width, \
props.walk none/both/left/right adds raised sidewalks walk_w wide) ending on another road's centre line makes a \
junction there; a road_curve starts at its x, y heading along local +x (rot 0/90/180/270), bends props.angle degrees \
left or right on radius r and ends where the next road starts; two roads ending at one point make a sharp corner. \
junction there (at any angle); a road_curve starts at its x, y heading along its rot, bends props.angle degrees \
left or right on radius r and ends where the next road starts; two roads ending at one point make a mitred corner. \
A roundabout (props.r ring radius, arm_e/n/w/s, arm_len) has its arm ends r + arm_len from its centre: put the \
roads' ends exactly there, with the same width as road_d.";
@@ -47,7 +50,7 @@ pub fn tools() -> Vec<Value> {
"x": { "type": "number", "description": "east, metres (centre of the footprint)" },
"y": { "type": "number", "description": "north, metres" },
"z": { "type": "number", "description": "base height, metres (default 0)" },
"rot": { "type": "number", "description": "rotation in degrees, multiples of 90" },
"rot": { "type": "number", "description": "rotation in degrees counter-clockwise from east, any angle (diagonal streets, walls, fences, hedges); 0, 90, 180, 270 keep to the axes" },
"props": { "type": "object", "description": "prefab properties (see list_prefabs); in updates they merge, null removes" },
"name": { "type": "string", "description": "name shown in the tooltip" },
"info": { "type": "string", "description": "tooltip text" },
+33 -1
View File
@@ -84,8 +84,19 @@ fn finite(v: f64, what: &str) -> Result<f64, String> {
}
}
/// Any angle, in [0, 360) to 4 decimals; within 0.001 degrees of a quarter
/// turn it is that quarter turn exactly (pieces along the axes stay exact).
fn norm_rot(r: f64) -> f64 {
(((r / 90.0).round() as i64).rem_euclid(4) * 90) as f64
let q = (r / 90.0).round();
if (r - q * 90.0).abs() < 1e-3 {
return ((q as i64).rem_euclid(4) * 90) as f64;
}
let a = (r.rem_euclid(360.0) * 1e4).round() / 1e4;
if a >= 360.0 {
0.0
} else {
a
}
}
/// Check property values against the prefab schema (unknown keys pass:
@@ -364,7 +375,28 @@ mod tests {
.id
.unwrap();
assert_eq!(id, "tree");
// any angle stays (diagonal streets and walls), normalised to [0, 360)
assert_eq!(d.objects[0].rot, 100.0);
apply(
&mut d,
&c,
op(json!({"op": "update", "id": "tree", "patch": {"rot": -45.000001}})),
)
.unwrap();
assert_eq!(d.objects[0].rot, 315.0);
apply(
&mut d,
&c,
op(json!({"op": "update", "id": "tree", "patch": {"rot": 450.0004}})),
)
.unwrap();
assert_eq!(d.objects[0].rot, 90.0);
apply(
&mut d,
&c,
op(json!({"op": "update", "id": "tree", "patch": {"rot": 100}})),
)
.unwrap();
apply(&mut d, &c, op(json!({"op": "update", "id": "tree", "patch": {"x": 7.5, "props": {"r": 2.0}, "tag": "Oak"}}))).unwrap();
assert_eq!(d.objects[0].x, 7.5);
assert_eq!(d.objects[0].props["r"], json!(2.0));