How to read a model's geometry¶
You have a parsed model and you want to know where its surfaces are: to draw them, to measure them, or to check what the file actually says. This guide turns a document into a resolved scene, in one frame, without editing the model to find out.
Reading geometry is the same operation in both languages and is spelled the same way. What a coordinate resolves to is a property of the EnergyPlus input format rather than of either library, so there is one set of instructions here and only the fenced code changes.
Differs in JavaScript
Reading geometry from a model exists in both libraries and does not behave the same way in JavaScript. The ledger records it as Python complete, JavaScript partial, and what differs is stated here rather than left to be discovered.
TypeScript carries the vector, the polygon and the resolved scene. getScene places
every detailed surface a model states and reports what it could not place and what it
did not attempt, so a reader who wants a model's geometry has it in both languages.
What it does not carry is the per-object calculation surface Python spells as free
functions over an IDF object: a zone's origin and its rotation, a surface's area, tilt
and azimuth read from the object rather than from a polygon, a zone's floor area,
ceiling area, height and volume, and the two-dimensional polygon predicates. Python's
translate_to_world has no counterpart either, and will not have the same one: it
resolves by mutating the document, where getScene resolves into a value and leaves the
model alone.
The full entry, including the vocabulary this capability owns, is on the capability parity page.
In JavaScript, geometry is a separate install
pip install idfkit installs geometry support unconditionally.
npm install @idfkit/idfkit does not. Add @idfkit/geometry by name. It
reads no files and holds no state, so it runs in a browser tab, a worker or
an edge runtime as readily as in Node.
Read the scene¶
# One argument, one return value, no options. Reading touches no disk and does
# not modify the document: writing it out before and after yields the same bytes.
scene = get_scene(doc)
for surface in scene.surfaces:
# object_type with name is the address. A name alone is not unique across
# types, so a consumer that stored only the name has to search to get back.
print(surface.object_type, surface.name, surface.zone)
# The vertices are already in the frame the engine computes. Nothing here
# has to be offset by a zone origin or turned by a north axis afterwards.
for vertex in surface.polygon.vertices:
print(vertex.x, vertex.y, vertex.z)
print(surface.area, surface.normal)
// One argument, one return value, no options. Reading touches no disk and does
// not modify the document: writing it out before and after yields the same bytes.
const scene = getScene(document);
for (const surface of scene.surfaces) {
// objectType with name is the address. A name alone is not unique across
// types, so a consumer that stored only the name has to search to get back.
console.log(surface.objectType, surface.name, surface.zone);
// The vertices are already in the frame the engine computes. Nothing here
// has to be offset by a zone origin or turned by a north axis afterwards.
for (const vertex of surface.polygon.vertices) {
console.log(vertex.x, vertex.y, vertex.z);
}
console.log(surface.area, surface.normal);
}
get_scene / getScene takes a document and returns a value. There is no
include_shading argument, no zone filter and no colour: filtering a list is
something you already know how to do, and a viewing decision belongs to the
thing doing the viewing.
The document is unchanged afterwards. Not "unchanged in the fields extraction reads": unchanged. A preserving write before and after yields identical bytes, which is what lets you resolve a model you are also editing.
The vertices are in the engine's frame¶
A surface's vertices as written in the file are not usually where the surface is. Up to three declarations move them, and all three are applied before you see a polygon:
- The coordinate system. Under
Relative, a surface's vertices are measured from its zone's origin, and the zone's ownDirection of Relative Northturns them first. UnderWorldneither applies. - The building north axis.
Building'sNorth Axisturns every surface about the world origin. There is one exception, below. - The entry direction. When the model declares clockwise entry, the ring is reversed, so the outward normal follows the right-hand rule in every model.
The starting vertex is left where the author put it. Rotating a ring does not change the surface, and renormalising it would silently discard which corner the file names first.
Shading:Site:Detailed does not turn with the building
Clause 2 has exactly one exception, and it is measured rather than assumed.
Site shading is fixed in space; Shading:Building:Detailed and
Shading:Zone:Detailed are not. A square entered identically under both
types in a model declaring a north axis of 158.434 comes back from
EnergyPlus 26.1.0 where it was authored under the first and turned under the
second, and the engine's own report labels them Detached Shading:Fixed and
Detached Shading:Building.
What the model declared, and what had to be assumed¶
A reader drawing a model usually wants the resolved vertices and nothing else. A reader checking a model wants to know what was read to get them.
applied = scene.applied
# What the model stated, in the schema's spelling rather than the file's casing.
print(applied.coordinate_system) # 'Relative' or 'World'
print(applied.vertex_entry_direction) # 'Counterclockwise' or 'Clockwise'
print(applied.starting_vertex_position) # 'UpperLeftCorner', and so on
print(applied.north_axis) # degrees, clockwise from true north
# Two conditions worth asking about directly, since both change the answer.
print(applied.is_relative, applied.is_clockwise)
# Fields the model did not state, named rather than silently defaulted.
if "north_axis" in applied.defaulted:
print("no Building north axis stated; resolution assumed 0")
const applied = scene.applied;
// What the model stated, in the schema's spelling rather than the file's casing.
console.log(applied.coordinateSystem); // 'Relative' or 'World'
console.log(applied.vertexEntryDirection); // 'Counterclockwise' or 'Clockwise'
console.log(applied.startingVertexPosition); // 'UpperLeftCorner', and so on
console.log(applied.northAxis); // degrees, clockwise from true north
// Two conditions worth asking about directly, since both change the answer.
console.log(applied.isRelative, applied.isClockwise);
// Fields the model did not state, named rather than silently defaulted.
if (applied.defaulted.includes('north_axis')) {
console.log('no Building north axis stated; resolution assumed 0');
}
defaulted holds the names of fields the model did not state, in the schema's
spelling, so the strings are the same in both languages. A model that states no
GlobalGeometryRules at all is read under the format's documented defaults and
says so here, which is a different fact from a model that states them and
happens to agree.
A stated zero is a declaration and is not listed. An absent or blank field is.
What could not be placed¶
A building with one bad wall is still a building you want to see, so nothing raises. An object that could not be resolved is reported instead.
for item in scene.unresolved:
# The reason is an enumerated value, not a message, so grouping on it is
# stable and a reworded string cannot change what a consumer does.
print(item.object_type, item.name, item.reason)
# 'zone-not-found' and 'parent-surface-not-found' also name what the object
# pointed at and the model does not hold. The reason says how to group the
# failure; this says which name to go and look for.
if item.missing_reference is not None:
print(" names", item.missing_reference)
for (const item of scene.unresolved) {
// The reason is an enumerated value, not a message, so grouping on it is
// stable and a reworded string cannot change what a consumer does.
console.log(item.objectType, item.name, item.reason);
// 'zone-not-found' and 'parent-surface-not-found' also name what the object
// pointed at and the model does not hold. The reason says how to group the
// failure; this says which name to go and look for.
if (item.missingReference !== undefined) {
console.log(' names', item.missingReference);
}
}
There are four reasons: no-vertices, too-few-vertices, zone-not-found and
parent-surface-not-found. The last two also carry the name the object pointed
at, because a reader fixing the model needs that name and should not have to
search the document a second time to get it.
Extraction raises only for a document it was not handed at all. Judging a model is validation's job; this reports.
A coordinate that is not a number is not yet one of the four
Measured against both libraries, on a model whose vertex field holds
autosize where a number belongs. Python raises ValueError: could not
convert string to float, which contradicts the paragraph above. TypeScript
does not raise and does something worse: it reports the surface as resolved
with NaN coordinates, and because the resolution mixes axes, one bad
coordinate leaves scene.bounds NaN in two axes for the whole model, so a
consumer framing a view gets nothing and is told nothing.
Neither is the documented answer, which is an entry in unresolved. The
four reasons above do not include one for this, and adding a fifth is a
registered concept in both languages rather than a local fix, so it is
recorded here until it is decided.
What was not read¶
Not every way of stating geometry is read yet. The simplified surface family states a surface as an origin, a width, a height and a tilt, which is a second rule set answerable only against a second set of engine output. Rather than skipping those objects, the scene names the types and counts them.
# A model whose geometry is stated in a form this reads nothing of comes back
# with no surfaces and a populated unattempted, which is a different answer from
# a model that holds no geometry at all.
if not scene.surfaces and scene.unattempted:
for entry in scene.unattempted:
print(f"{entry.object_type}: {entry.count} not read")
# Everything is accounted for: resolved, unresolved with a reason, or of a type
# recorded as unattempted. There is no fourth outcome and nothing is dropped.
total = len(scene.surfaces) + len(scene.unresolved) + sum(e.count for e in scene.unattempted)
print(total)
// A model whose geometry is stated in a form this reads nothing of comes back
// with no surfaces and a populated unattempted, which is a different answer from
// a model that holds no geometry at all.
if (scene.surfaces.length === 0 && scene.unattempted.length > 0) {
for (const entry of scene.unattempted) {
console.log(`${entry.objectType}: ${entry.count} not read`);
}
}
// Everything is accounted for: resolved, unresolved with a reason, or of a type
// recorded as unattempted. There is no fourth outcome and nothing is dropped.
const total =
scene.surfaces.length +
scene.unresolved.length +
scene.unattempted.reduce((sum, entry) => sum + entry.count, 0);
console.log(total);
That accounting is the guarantee worth relying on: every geometry object in the model is resolved, unresolved with a reason, or of a type recorded as unattempted. There is no fourth outcome, so a wall cannot go missing without a word.
The extent of what was placed¶
bounds = scene.bounds
# Absent rather than zero when nothing was placed, so an empty model and a model
# sitting on the origin are distinguishable.
if bounds is None:
print("nothing resolved")
else:
print(bounds.min.x, bounds.min.y, bounds.min.z)
print(bounds.max.x, bounds.max.y, bounds.max.z)
centre_x = (bounds.min.x + bounds.max.x) / 2
centre_y = (bounds.min.y + bounds.max.y) / 2
print(centre_x, centre_y)
const bounds = scene.bounds;
// Absent rather than zero when nothing was placed, so an empty model and a model
// sitting on the origin are distinguishable.
if (bounds === undefined) {
console.log('nothing resolved');
} else {
console.log(bounds.min.x, bounds.min.y, bounds.min.z);
console.log(bounds.max.x, bounds.max.y, bounds.max.z);
const centreX = (bounds.min.x + bounds.max.x) / 2;
const centreY = (bounds.min.y + bounds.max.y) / 2;
console.log(centreX, centreY);
}
The bounds enclose the resolved vertices and nothing else. They are absent rather than zero when nothing was placed, so a model with no geometry and a model sitting on the origin are distinguishable.
Rewriting the document instead (Python only)¶
Everything above reads. Python also has translate_to_world, which resolves
through the same rule and writes the result back into the document, then
restates what the vertices were resolved against so a second call applies
nothing again.
# Python only, and a mutation rather than a reading. It resolves through the same
# rule and writes the resolved vertices back into the document, then restates
# what they were resolved against so that a second call applies nothing again.
translate_to_world(doc)
# An object the resolution could not place is left in the frame it was authored
# in, and is named in a warning on the idfkit logger rather than dropped.
Prefer the scene unless you specifically want the model changed. Everything above is available from a document you are not allowed to edit; this is not.
It cannot make a mixed model consistent
translate_to_world rewrites only the five detailed vertex types, and it
zeroes Building.north_axis when it is done. An object stated in the
simplified surface family, and Daylighting:ReferencePoint, carries its own
coordinate-system field, is not read, and is not rewritten. In a model that
holds both, those objects keep the coordinates the author wrote while the
declaration they were written against is removed from underneath them. The
scene reports such types in unattempted instead of editing the document,
and is the right tool there.
Until idfkit 1.0.0-rc.6 this function carried a resolution rule of its own and disagreed with both the engine and the renderer. It now resolves through the scene, so the two cannot drift apart again. Models it draws differently than before are listed in the library's changelog.
See also¶
- What parity means for how availability is recorded and where this capability stands
- Geometry API reference for the Python surface in full
@idfkit/geometryfor the TypeScript surface in full