Valerii Kozhevets about work study certificatescerts projects tools

Digital twin of Spoordok, Leeuwarden

A 3D planning tool for a real Leeuwarden district, with a local vision model that scores eye-level views, built on my own and chosen by the Municipality of Leeuwarden as the best concept of the student teams.

2025–2026 3D prototypingsoftwareAI

Digital twin of Spoordok, Leeuwarden
Client Municipality of Leeuwarden, through an NHL Stenden project module
Result chosen as the best concept of the student teams, 16 February 2026
Shown at NHL Stenden, Leeuwarden city hall, Leeuwarder Digitale Agenda demo day, Rijksontmoetingsplein opening
Front end CesiumJS 1.135 and plain ES modules, on satellite or OpenStreetMap imagery
Back end Flask in front of Ollama, started by one script or Docker Compose
AI a local vision model through Ollama (qwen3.5:9b by default) scores eye-level views and answers questions about the plan
Saving one JSON file per plan, with a versioned schema and migrations

A browser-based 3D planning tool for Spoordok, the strip between the Harlingervaart canal and the railway just west of Leeuwarden station, which the city is redeveloping into a new district. The Municipality of Leeuwarden set the assignment as a competition between student teams of three to five. Mine started as three; the other two left early, so I built my entry on my own, and at the final pitch in Leeuwarden city hall on 16 February 2026 the municipality chose it as the best concept. Buildings, roads, parks and parking are drawn onto the real 21.6-hectare site, and cost, inhabitants, workers, green space and parking are measured against planning targets as each shape is placed. Observers set down inside the plan capture what a person standing there would see, and a vision model running on the same machine scores every view.

The first version, December 2025

The assignment ran as Project Digital Twin, a second-year module of NHL Stenden’s HBO-ICT programme that I took in my first year. I showed the first working version at NHL Stenden on 18 December 2025. It opened on a setup screen for the goals, the building types and the site boundary, then showed Spoordok on a CesiumJS globe over OpenStreetMap, where footprints were drawn and extruded into buildings. Eight types came with it: detached house, row house, apartment, business building, park, road, surface parking and parking garage. Buildings are costed and occupied per cubic metre of volume; parks, roads and parking per square metre, and the two parking types also yield spaces. A statistics panel measured the plan against the default goals: at least 3,000 inhabitants, 500 workers and 20% green space, business buildings on at most 20% of the drawn area, and 4,500 parking spaces. A whole plan saved to one JSON file. The AI panel was still a placeholder.

Valerii pointing at a lecture-hall screen showing the first version: flat coloured building blocks on a pale map, beside an NHL Stenden lectern
The first working version, shown at NHL Stenden on 18 December 2025.

Over the next two days came solar and green roofs, satellite imagery as an alternative to the map, and the existing city around the site as Cesium’s OpenStreetMap 3D buildings, clipped away inside the site boundary so that a plan replaces what stands there today and sits among the real blocks around it.

The evaluation mode and the pitch, February 2026

Cost and occupancy do not say what a street feels like, which is what the evaluation mode is for. It was built for the pitch: a Flask back end in front of Ollama, and a four-step flow in which observers are placed, their views are captured and analysed, and the results are read. In that first version one model (qwen3-vl:8b) described each captured view and a text model (qwen3:8b) scored the description.

The final pitch was on 16 February 2026 in the council chamber of Leeuwarden city hall, where the student teams presented to the municipality. The municipality chose mine as the best concept and invited me to present it at the Leeuwarder Digitale Agenda demo day. The municipality’s project contacts told me they were impressed by the technical depth of the AI integration and looked forward to working out how to use it in practice.

Valerii at a lectern in the council chamber, beside a large screen showing the application's AI liveability analysis over the 3D city
The final pitch in the council chamber, 16 February 2026, with the evaluation mode on the chamber screen.
the application in February 2026: flat coloured blocks on an OpenStreetMap base map, a statistics and goals panel on the right and an AI liveability score of 6.0 out of 10
The tool on 17 February 2026, the day after the pitch. Spline roads, parked cars and trees came later.

On 21 February the vision model started scoring each view itself, in one step, and a chat assistant was added that answers questions about a finished evaluation with every score in its context.

Demo day and the Rijksontmoetingsplein, March to May 2026

I presented that version at the Leeuwarder Digitale Agenda demo day on 26 March 2026.

Valerii speaking into a microphone in front of a large screen showing the application's project configuration screen
Presenting at the Leeuwarder Digitale Agenda demo day, 26 March 2026.Video still: Thomas Keegstra and Stefan Brattinga

On 18 May 2026 I showed it at the opening of the Rijksontmoetingsplein in Leeuwarden, on the NHL Stenden stand, where the mayor and senior municipal officials stopped to see it.

Streets, parking and trees, June to September 2026

On 30 June parks got trees, and the Docker build was pinned to qwen3.5:9b, one model for both vision and chat. By July the plan had spline roads, parking lots filled with cars, solar panels on the roofs and procedural facade textures. From 27 August to 9 September I reworked the application: the setup screen became a Params dialog, so the map is the first thing on screen; the tools became four planning layers (buildings, roads, green, parking); building types can be added, edited and deleted, except one that a building still uses; every edit can be undone, up to the last 100; and a saved file carries a schema version, is migrated forward, and is validated in full before anything on the map is replaced. The application runs locally from one start script, or as three Docker Compose services: the front end, the Flask back end, and Ollama on the GPU.

the application on satellite imagery: tool rail on the left, the selected building's inspector, the planned district in the middle and the statistics and goals panel on the right
The plan on satellite imagery, 9 September 2026, looking north to the Harlingervaart canal. Selecting a building opens its inspector, and a goal bar turns red where the plan misses its target.
Drawing one building: a click for each corner. The cost, the building count and the goal bars update when the outline closes.

Roads are Catmull-Rom splines on a shared node graph. Where a new road crosses an old one, a junction node is inserted automatically, and dragging that node reshapes every road that uses it. Any junction can become a roundabout with its own ring width and grass island. Pressing E pulls a new road from any selected point, and every point carries its own width, set with S, so a road can widen or narrow along its length. A road can close into a ring and can cross itself; a closed road that crosses itself is not one polygon, so it is cut at every repeated node into the separate rings it really is before its surface is built.

2 October 2026, in the app: a junction turned into a roundabout, a branch pulled from a new point with E, and its far end widened with S.
2 October 2026, in the app: the empty site built into a district by a script that drives the app's own road and building tools. First 19 roads with six roundabouts, then 166 footprints: houses, apartment blocks, business buildings, parks and parking lots, with the trees, parked cars and solar roofs the app generates for them.

Building outlines can cross themselves too: an outline traced with a crossing in it lost area, because the two lobes of the shoelace sum cancel, and a symmetric bowtie measured 0 m² and cost next to nothing. Such an outline is untangled when it closes: every corner stays where it was placed, and only the order they are joined in changes, by reversing the run of corners between the two crossing edges until no edge crosses another.

Parking lots are filled by a stall-packing solver: 2.5 × 5 m stalls in double rows with 6 m aisles, turned to whichever direction fits the most stalls, with cars clustered near the entrance. Trees in a park are scattered from a seed made of the park’s id and footprint, so the same park always gets the same trees, and each park is drawn as one batched primitive. The batching cut the test scene from 2,857 Cesium entities to 1,135, and re-solving only the parking lots a road change touches cut a road edit through a lot from 31 ms to about 6 ms.

The same plan in both views. The flat one drops every building to its footprint and colours it by type: business yellow, housing blue-grey, parking grey, green space green.

The evaluation rebuilt, September 2026

In September I rebuilt the evaluation around one camera pose per observer, shared by the preview and the captured photo. An observer is placed with a click on the ground and aimed by dragging, with an eye height from 1.5 to 12 m and a field of view from 60 to 120°.

placing observers on the plan, with a coloured view wedge on the ground for each one
Each wedge on the ground is the field of view that observer's photo is taken with.

Each capture goes to the vision model with a prompt that says where it stands, as in “from 1.7 m above the ground, looking north-west”, and the model has to answer with JSON matching a schema: a description, an analysis and a whole-number score from 0 to 10. With the model’s thinking turned off and its output constrained to that schema, a photo went from 160 seconds to about 11 once the model is loaded. At temperature 0.7 the same image scored 4, 8 and 4 on three runs; at 0.15 it scored 5, 4 and 5, so the analysis now runs at 0.15. The chat model gets every score, each observer’s eye height and direction, and the plan’s own numbers, and can be asked what to change. Every score is saved with the position, heading, eye height and field of view it came from, so a low score can be traced back to a spot in the plan.

At Spoordok’s latitude (53.2° N) a degree of longitude covers 0.6 of the ground distance of a degree of latitude, so taking the angle straight from the coordinate differences read a point exactly north-east as 59° instead of 45°, and every observer looked somewhere other than where its arrow pointed. The bearing is now computed in ground metres, with longitude scaled by the cosine of the latitude, and a test checks both the right answer and that the old formula would miss by more than 10°.

the AI liveability analysis window: an overall score of 3.9 out of 10 from seven observers, and the first two observers' views with the model's written assessment and score beside each
The top of the analysis window from a run on 9 September 2026 with seven observers, who scored 2, 4, 3, 4, 6, 3 and 5. The plan's liveability score is their average.
apartment blocks behind a row of trees, with a parking lot in the foreground, seen at eye level
Eye level inside the plan, 9 September 2026. The trees, the parked cars and the facade textures are all generated from the footprints and lots drawn on the map.

The other way I get a real place into a 3D scene is photogrammetry, from photographs instead of a map.

Related