Skip to content

Commit 8ea89c7

Browse files
committed
Adds (yet untested) 'offset' & 'outline' methods
1 parent 1f8df35 commit 8ea89c7

File tree

1 file changed

+355
-2
lines changed

1 file changed

+355
-2
lines changed

src/osut/osut.py

Lines changed: 355 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4030,7 +4030,6 @@ def overlap(p1=None, p2=None, flat=False) -> bool:
40304030

40314031
area1 = openstudio.getArea(a1)
40324032
area2 = openstudio.getArea(a2)
4033-
40344033
if not area1: return oslg.empty("points 1 area", mth, CN.ERR, face)
40354034
if not area2: return oslg.empty("points 2 area", mth, CN.ERR, face)
40364035

@@ -4066,7 +4065,7 @@ def overlap(p1=None, p2=None, flat=False) -> bool:
40664065
return p3Dv(res1)
40674066

40684067

4069-
def overlapping(p1=None, p2=None, flat=False):
4068+
def overlapping(p1=None, p2=None, flat=False) -> bool:
40704069
"""Determines whether OpenStudio polygons overlap.
40714070
40724071
Args:
@@ -4136,6 +4135,360 @@ def cast(p1=None, p2=None, ray=None) -> openstudio.Point3dVector:
41364135
return face
41374136

41384137

4138+
def offset(p1=None, w=0, v=0) -> openstudio.Point3dVector:
4139+
"""Generates offset vertices (by width) for a 3- or 4-sided, convex polygon.
4140+
If width is negative, the vertices are contracted inwards.
4141+
4142+
Args:
4143+
p1 (openstudio.Point3dVector):
4144+
OpenStudio vector of 3D points.
4145+
w (float):
4146+
Offset width (absolute min: 0.0254m).
4147+
v (int):
4148+
OpenStudio SDK version, eg '321' for "v3.2.1" (optional).
4149+
4150+
Returns:
4151+
openstudio.Point3dVector: Offset points (see logs if unaltered).
4152+
4153+
"""
4154+
mth = "osut.offset"
4155+
vs = int("".join(openstudio.openStudioVersion().split(".")))
4156+
pts = poly(p1, True, True, False, True, "cw")
4157+
4158+
if len(pts) < 3 or len(pts) > 4:
4159+
return oslg.invalid("points", mth, 1, CN.DBG, p1)
4160+
elif len(pts) == 4:
4161+
iv = True
4162+
else:
4163+
iv = False
4164+
4165+
try:
4166+
w = float(w)
4167+
except:
4168+
oslg.mismatch("width", w, float, mth)
4169+
w = 0
4170+
4171+
try:
4172+
v = int(v)
4173+
except:
4174+
oslg.mismatch("version", v, int, mth)
4175+
v = vs
4176+
4177+
if abs(w) < 0.0254: return p1
4178+
4179+
if v >= 340:
4180+
t = openstudio.Transformation.alignFace(p1)
4181+
offset = openstudio.buffer(pts, w, CN.TOL)
4182+
if not offset: return p1
4183+
4184+
offset = offset.get()
4185+
offset.reverse()
4186+
return to_p3Dv(list(t * offset))
4187+
else: # brute force approach
4188+
pz = {}
4189+
pz["A"] = {}
4190+
pz["B"] = {}
4191+
pz["C"] = {}
4192+
if iv:
4193+
pz["D"] = {}
4194+
4195+
pz["A"]["p"] = openstudio.Point3d(p1[0].x(), p1[0].y(), p1[0].z())
4196+
pz["B"]["p"] = openstudio.Point3d(p1[1].x(), p1[1].y(), p1[1].z())
4197+
pz["C"]["p"] = openstudio.Point3d(p1[2].x(), p1[2].y(), p1[2].z())
4198+
if iv:
4199+
pz["D"]["p"] = openstudio.Point3d(p1[3].x(), p1[3].y(), p1[3].z())
4200+
4201+
pzAp = pz["A"]["p"]
4202+
pzBp = pz["B"]["p"]
4203+
pzCp = pz["C"]["p"]
4204+
if iv:
4205+
pzDp = pz["D"]["p"]
4206+
4207+
# Generate vector pairs, from next point & from previous point.
4208+
# :f_n : "from next"
4209+
# :f_p : "from previous"
4210+
#
4211+
#
4212+
#
4213+
#
4214+
#
4215+
#
4216+
# A <---------- B
4217+
# ^
4218+
# \
4219+
# \
4220+
# C (or D)
4221+
#
4222+
pz["A"]["f_n"] = pzAp - pzBp
4223+
if iv:
4224+
pz["A"]["f_p"] = pzAp - pzDp
4225+
else:
4226+
pz["A"]["f_p"] = pzAp - pzCp
4227+
4228+
pz["B"]["f_n"] = pzBp - pzCp
4229+
pz["B"]["f_p"] = pzBp - pzAp
4230+
4231+
pz["C"]["f_p"] = pzCp - pzBp
4232+
if iv:
4233+
pz["C"]["f_n"] = pzCp - pzDp
4234+
else:
4235+
pz["C"]["f_n"] = pzCp - pzAp
4236+
4237+
if iv:
4238+
pz["D"]["f_n"] = pzDp - pzAp
4239+
pz["D"]["f_p"] = pzDp - pzCp
4240+
4241+
# Generate 3D plane from vectors.
4242+
#
4243+
#
4244+
# | <<< 3D plane ... from point A, with normal B>A
4245+
# |
4246+
# |
4247+
# |
4248+
# <---------- A <---------- B
4249+
# |\
4250+
# | \
4251+
# | \
4252+
# | C (or D)
4253+
#
4254+
pz["A"]["pl_f_n"] = openstudio.Plane(pzAp, pz["A"]["f_n"])
4255+
pz["A"]["pl_f_p"] = openstudio.Plane(pzAp, pz["A"]["f_p"])
4256+
4257+
pz["B"]["pl_f_n"] = openstudio.Plane(pzBp, pz["B"]["f_n"])
4258+
pz["B"]["pl_f_p"] = openstudio.Plane(pzBp, pz["B"]["f_p"])
4259+
4260+
pz["C"]["pl_f_n"] = openstudio.Plane(pzCp, pz["C"]["f_n"])
4261+
pz["C"]["pl_f_p"] = openstudio.Plane(pzCp, pz["C"]["f_p"])
4262+
4263+
if iv:
4264+
pz["D"]["pl_f_n"] = openstudio.Plane(pzDp, pz["D"]["f_n"])
4265+
pz["D"]["pl_f_p"] = openstudio.Plane(pzDp, pz["D"]["f_p"])
4266+
4267+
# Project an extended point (pC) unto 3D plane.
4268+
#
4269+
# pC <<< projected unto extended B>A 3D plane
4270+
# eC |
4271+
# \ |
4272+
# \ |
4273+
# \|
4274+
# <---------- A <---------- B
4275+
# |\
4276+
# | \
4277+
# | \
4278+
# | C (or D)
4279+
#
4280+
pz["A"]["p_n_pl"] = pz["A"]["pl_f_n"].project(pz["A"]["p"] + pz["A"]["f_p"])
4281+
pz["A"]["n_p_pl"] = pz["A"]["pl_f_p"].project(pz["A"]["p"] + pz["A"]["f_n"])
4282+
4283+
pz["B"]["p_n_pl"] = pz["B"]["pl_f_n"].project(pz["B"]["p"] + pz["B"]["f_p"])
4284+
pz["B"]["n_p_pl"] = pz["B"]["pl_f_p"].project(pz["B"]["p"] + pz["B"]["f_n"])
4285+
4286+
pz["C"]["p_n_pl"] = pz["C"]["pl_f_n"].project(pz["C"]["p"] + pz["C"]["f_p"])
4287+
pz["C"]["n_p_pl"] = pz["C"]["pl_f_p"].project(pz["C"]["p"] + pz["C"]["f_n"])
4288+
4289+
if iv:
4290+
pz["D"]["p_n_pl"] = pz["D"]["pl_f_n"].project(pz["D"]["p"] + pz["D"]["f_p"])
4291+
pz["D"]["n_p_pl"] = pz["D"]["pl_f_p"].project(pz["D"]["p"] + pz["D"]["f_n"])
4292+
4293+
# Generate vector from point (e.g. A) to projected extended point (pC).
4294+
#
4295+
# pC
4296+
# eC ^
4297+
# \ |
4298+
# \ |
4299+
# \|
4300+
# <---------- A <---------- B
4301+
# |\
4302+
# | \
4303+
# | \
4304+
# | C (or D)
4305+
#
4306+
pz["A"]["n_p_n_pl"] = pz["A"]["p_n_pl"] - pzAp
4307+
pz["A"]["n_n_p_pl"] = pz["A"]["n_p_pl"] - pzAp
4308+
4309+
pz["B"]["n_p_n_pl"] = pz["B"]["p_n_pl"] - pzBp
4310+
pz["B"]["n_n_p_pl"] = pz["B"]["n_p_pl"] - pzBp
4311+
4312+
pz["C"]["n_p_n_pl"] = pz["C"]["p_n_pl"] - pzCp
4313+
pz["C"]["n_n_p_pl"] = pz["C"]["n_p_pl"] - pzCp
4314+
4315+
if iv:
4316+
pz["D"]["n_p_n_pl"] = pz["D"]["p_n_pl"] - pzDp
4317+
pz["D"]["n_n_p_pl"] = pz["D"]["n_p_pl"] - pzDp
4318+
4319+
# Fetch angle between both extended vectors (A>pC & A>pB),
4320+
# ... then normalize (Cn).
4321+
#
4322+
# pC
4323+
# eC ^
4324+
# \ |
4325+
# \ Cn
4326+
# \|
4327+
# <---------- A <---------- B
4328+
# |\
4329+
# | \
4330+
# | \
4331+
# | C (or D)
4332+
#
4333+
a1 = openstudio.getAngle(pz["A"]["n_p_n_pl"], pz["A"]["n_n_p_pl"])
4334+
a2 = openstudio.getAngle(pz["B"]["n_p_n_pl"], pz["B"]["n_n_p_pl"])
4335+
a3 = openstudio.getAngle(pz["C"]["n_p_n_pl"], pz["C"]["n_n_p_pl"])
4336+
if iv:
4337+
a4 = openstudio.getAngle(pz["D"]["n_p_n_pl"], pz["D"]["n_n_p_pl"])
4338+
4339+
# Generate new 3D points A', B', C' (and D') ... zigzag.
4340+
#
4341+
#
4342+
#
4343+
#
4344+
# A' ---------------------- B'
4345+
# \
4346+
# \ A <---------- B
4347+
# \ \
4348+
# \ \
4349+
# \ \
4350+
# C' C
4351+
pz["A"]["f_n"].normalize()
4352+
pz["A"]["n_p_n_pl"].normalize()
4353+
pzAp = pzAp + scalar(pz["A"]["n_p_n_pl"], w)
4354+
pzAp = pzAp + scalar(pz["A"]["f_n"], w * math.tan(a1/2))
4355+
4356+
pz["B"]["f_n"].normalize()
4357+
pz["B"]["n_p_n_pl"].normalize()
4358+
pzBp = pzBp + scalar(pz["B"]["n_p_n_pl"], w)
4359+
pzBp = pzBp + scalar(pz["B"]["f_n"], w * math.tan(a2/2))
4360+
4361+
pz["C"]["f_n"].normalize()
4362+
pz["C"]["n_p_n_pl"].normalize()
4363+
pzCp = pzCp + scalar(pz["C"]["n_p_n_pl"], w)
4364+
pzCp = pzCp + scalar(pz["C"]["f_n"], w * math.tan(a3/2))
4365+
4366+
if iv:
4367+
pz["D"]["f_n"].normalize()
4368+
pz["D"]["n_p_n_pl"].normalize()
4369+
pzDp = pzDp + scalar(pz["D"]["n_p_n_pl"], w)
4370+
pzDp = pzDp + scalar(pz["D"]["f_n"], w * math.tan(a4/2))
4371+
4372+
# Re-convert to OpenStudio 3D points.
4373+
vec = openstudio.Point3dVector()
4374+
vec.append(openstudio.Point3d(pzAp.x(), pzAp.y(), pzAp.z()))
4375+
vec.append(openstudio.Point3d(pzBp.x(), pzBp.y(), pzBp.z()))
4376+
vec.append(openstudio.Point3d(pzCp.x(), pzCp.y(), pzCp.z()))
4377+
if iv:
4378+
vec.append(openstudio.Point3d(pzDp.x(), pzDp.y(), pzDp.z()))
4379+
4380+
return vec
4381+
4382+
4383+
def outline(a=[], bfr=0, flat=True) -> openstudio.Point3dVector:
4384+
"""Generates a ULC OpenStudio 3D point vector (a bounding box) that
4385+
surrounds multiple (smaller) OpenStudio 3D point vectors. The generated,
4386+
4-point outline is optionally buffered (or offset). Frame and Divider frame
4387+
widths are taken into account.
4388+
4389+
Args:
4390+
a (list):
4391+
One or more sets of OpenStudio 3D points.
4392+
bfr (float):
4393+
An optional buffer size (min: 0.0254m).
4394+
flat (bool):
4395+
Whether points are to be pre-flattened (Z=0).
4396+
Returns:
4397+
openstudio.Point3dVector: ULC outline (see logs if empty).
4398+
4399+
"""
4400+
mth = "osut.outline"
4401+
out = openstudio.Point3dVector()
4402+
xMIN = None
4403+
xMAX = None
4404+
yMIN = None
4405+
yMAX = None
4406+
a2 = []
4407+
4408+
try:
4409+
bfr = float(bfr)
4410+
except:
4411+
oslg.mismatch("buffer", bfr, float, mth)
4412+
bfr = 0
4413+
4414+
try:
4415+
flat = bool(flat)
4416+
except:
4417+
flat = True
4418+
4419+
try:
4420+
a = list(a)
4421+
except:
4422+
return oslg.mismatch("array", a, list, mth, CN.DBG, out)
4423+
4424+
if not a: return oslg.empty("array", mth, CN.DBG, out)
4425+
4426+
vtx = poly(a[0])
4427+
if not vtx: return out
4428+
if bfr < 0.0254: bfr = 0
4429+
4430+
t = openstudio.Transformation.alignFace(vtx)
4431+
4432+
for pts in a:
4433+
points = poly(pts, False, True, False, t)
4434+
if flat: points = flatten(points)
4435+
if not points: continue
4436+
4437+
a2.append(points)
4438+
4439+
for pts in a2:
4440+
xs = [pt.x() for pt in pts]
4441+
ys = [pt.y() for pt in pts]
4442+
4443+
minX = min(xs)
4444+
maxX = max(xs)
4445+
minY = min(ys)
4446+
maxY = max(ys)
4447+
4448+
# Consider frame width, if frame-and-divider-enabled sub surface.
4449+
if hasattr(pts, "allowWindowPropertyFrameAndDivider"):
4450+
w = 0
4451+
fd = pts.windowPropertyFrameAndDivider()
4452+
if fd: w = fd.get().frameWidth()
4453+
4454+
if w > CN.TOL:
4455+
minX -= w
4456+
maxX += w
4457+
minY -= w
4458+
maxY += w
4459+
4460+
if not xMIN: xMIN = minX
4461+
if not xMAX: xMAX = maxX
4462+
if not yMIN: yMIN = minY
4463+
if not yMAX: yMAX = maxY
4464+
4465+
xMIN = min(xMIN, minX)
4466+
xMAX = max(xMAX, maxX)
4467+
yMIN = min(yMIN, minY)
4468+
yMAX = max(yMAX, maxY)
4469+
4470+
if xMAX < xMIN:
4471+
return oslg.negative("outline width", mth, CN.DBG, out)
4472+
if yMAX < yMIN:
4473+
return oslg.negative("outline height", mth, Cn.DBG, out)
4474+
if abs(xMIN - xMAX) < TOL:
4475+
return oslg.zero("outline width", mth, CN.DBG, out)
4476+
if abs(yMIN - yMAX) < TOL:
4477+
return oslg.zero("outline height", mth, CN.DBG, out)
4478+
4479+
# Generate ULC point 3D vector.
4480+
out.append(openstudio.Point3d(xMIN, yMAX, 0))
4481+
out.append(openstudio.Point3d(xMIN, yMIN, 0))
4482+
out.append(openstudio.Point3d(xMAX, yMIN, 0))
4483+
out.append(openstudio.Point3d(xMAX, yMAX, 0))
4484+
4485+
# Apply buffer, apply ULC (options).
4486+
if bfr > 0.0254: out = offset(out, bfr, 300)
4487+
4488+
return to_p3Dv(t * out)
4489+
4490+
4491+
41394492
def facets(spaces=[], boundary="all", type="all", sides=[]) -> list:
41404493
"""Returns an array of OpenStudio space surfaces or subsurfaces that match
41414494
criteria, e.g. exterior, north-east facing walls in hotel "lobby". Note

0 commit comments

Comments
 (0)