564 lines
18 KiB
C
564 lines
18 KiB
C
// BLUENOISE :: https://github.com/prideout/par
|
|
// Generator for infinite 2D point sequences using Recursive Wang Tiles.
|
|
//
|
|
// In addition to this source code, you'll need to download one of the following
|
|
// tilesets, the first being 2 MB while the other is 257 KB. The latter cheats
|
|
// by referencing the point sequence from the first tile for all 8 tiles. This
|
|
// obviously produces poor results, but in many contexts, it isn't noticeable.
|
|
//
|
|
// https://prideout.net/assets/bluenoise.bin
|
|
// https://prideout.net/assets/bluenoise.trimmed.bin
|
|
//
|
|
// The code herein is an implementation of the algorithm described in:
|
|
//
|
|
// Recursive Wang Tiles for Real-Time Blue Noise
|
|
// Johannes Kopf, Daniel Cohen-Or, Oliver Deussen, Dani Lischinski
|
|
// ACM Transactions on Graphics 25, 3 (Proc. SIGGRAPH 2006)
|
|
//
|
|
// If you use this software for research purposes, please cite the above paper
|
|
// in any resulting publication.
|
|
//
|
|
// EXAMPLE
|
|
//
|
|
// Generate point samples whose density is guided by a 512x512 grayscale image:
|
|
//
|
|
// int npoints;
|
|
// float* points;
|
|
// int maxpoints = 1e6;
|
|
// float density = 30000;
|
|
// par_bluenoise_context* ctx;
|
|
// ctx = par_bluenoise_from_file("bluenoise.bin", maxpoints);
|
|
// par_bluenoise_density_from_gray(ctx, source_pixels, 512, 512, 1);
|
|
// points = par_bluenoise_generate(ctx, density, &npoints);
|
|
// ... Draw points here. Each point is a three-tuple of (X Y RANK).
|
|
// par_bluenoise_free(ctx);
|
|
//
|
|
// Distributed under the MIT License, see bottom of file.
|
|
|
|
#ifndef PAR_BLUENOISE_H
|
|
#define PAR_BLUENOISE_H
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// BEGIN PUBLIC API
|
|
// -----------------------------------------------------------------------------
|
|
|
|
typedef unsigned char par_byte;
|
|
|
|
// Encapsulates a tile set and an optional density function.
|
|
typedef struct par_bluenoise_context_s par_bluenoise_context;
|
|
|
|
// Creates a bluenoise context using the given tileset. The first argument is
|
|
// the file path the bin file. The second argument is the maximum number of
|
|
// points that you expect to ever be generated.
|
|
par_bluenoise_context* par_bluenoise_from_file(char const* path, int maxpts);
|
|
|
|
// Creates a bluenoise context using the given tileset. The first and second
|
|
// arguments describe a memory buffer containing the contents of the bin file.
|
|
// The third argument is the maximum number of points that you expect to ever
|
|
// be generated.
|
|
par_bluenoise_context* par_bluenoise_from_buffer(
|
|
par_byte const* buffer, int nbytes, int maxpts);
|
|
|
|
// Sets up a scissoring rectangle using the given lower-left and upper-right
|
|
// coordinates. By default the scissor encompasses [-0.5, -0.5] - [0.5, 0.5],
|
|
// which is the entire sampling domain for the two "generate" methods.
|
|
void par_bluenoise_set_viewport(
|
|
par_bluenoise_context*, float left, float bottom, float right, float top);
|
|
|
|
// Sets up a reference window size. The only purpose of this is to ensure
|
|
// that apparent density remains constant when the window gets resized.
|
|
// Clients should call this *before* calling par_bluenoise_set_viewport.
|
|
void par_bluenoise_set_window(par_bluenoise_context*, int width, int height);
|
|
|
|
// Frees all memory associated with the given bluenoise context.
|
|
void par_bluenoise_free(par_bluenoise_context* ctx);
|
|
|
|
// Copies a grayscale image into the bluenoise context to guide point density.
|
|
// Darker regions generate a higher number of points. The given bytes-per-pixel
|
|
// value is the stride between pixels.
|
|
void par_bluenoise_density_from_gray(par_bluenoise_context* ctx,
|
|
const unsigned char* pixels, int width, int height, int bpp);
|
|
|
|
// Creates a binary mask to guide point density. The given bytes-per-pixel
|
|
// value is the stride between pixels, which must be 4 or less.
|
|
void par_bluenoise_density_from_color(par_bluenoise_context* ctx,
|
|
const unsigned char* pixels, int width, int height, int bpp,
|
|
unsigned int background_color, int invert);
|
|
|
|
// Generates samples using Recursive Wang Tiles. This is really fast!
|
|
// The returned pointer is a list of three-tuples, where XY are in [-0.5, +0.5]
|
|
// and Z is a rank value that can be used to create a progressive ordering.
|
|
// The caller should not free the returned pointer.
|
|
float* par_bluenoise_generate(
|
|
par_bluenoise_context* ctx, float density, int* npts);
|
|
|
|
// Generates an ordered sequence of tuples with the specified sequence length.
|
|
// This is slower than the other "generate" method because it uses a dumb
|
|
// backtracking method to determine a reasonable density value, and it
|
|
// automatically sorts the output by rank. The dims argument must be 2 or more;
|
|
// it represents the desired stride (in floats) between consecutive verts in the
|
|
// returned data buffer.
|
|
float* par_bluenoise_generate_exact(
|
|
par_bluenoise_context* ctx, int npts, int dims);
|
|
|
|
// Performs an in-place sort of 3-tuples, based on the 3rd component, then
|
|
// replaces the 3rd component with an index.
|
|
void par_bluenoise_sort_by_rank(float* pts, int npts);
|
|
|
|
#ifndef PAR_PI
|
|
#define PAR_PI (3.14159265359)
|
|
#define PAR_MIN(a, b) (a > b ? b : a)
|
|
#define PAR_MAX(a, b) (a > b ? a : b)
|
|
#define PAR_CLAMP(v, lo, hi) PAR_MAX(lo, PAR_MIN(hi, v))
|
|
#define PAR_SWAP(T, A, B) { T tmp = B; B = A; A = tmp; }
|
|
#define PAR_SQR(a) ((a) * (a))
|
|
#endif
|
|
|
|
#ifndef PAR_MALLOC
|
|
#define PAR_MALLOC(T, N) ((T*) malloc(N * sizeof(T)))
|
|
#define PAR_CALLOC(T, N) ((T*) calloc(N * sizeof(T), 1))
|
|
#define PAR_REALLOC(T, BUF, N) ((T*) realloc(BUF, sizeof(T) * (N)))
|
|
#define PAR_FREE(BUF) free(BUF)
|
|
#endif
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// END PUBLIC API
|
|
// -----------------------------------------------------------------------------
|
|
|
|
#ifdef PAR_BLUENOISE_IMPLEMENTATION
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <assert.h>
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
#define PAR_MINI(a, b) ((a < b) ? a : b)
|
|
#define PAR_MAXI(a, b) ((a > b) ? a : b)
|
|
|
|
typedef struct {
|
|
float x;
|
|
float y;
|
|
} par_vec2;
|
|
|
|
typedef struct {
|
|
float x;
|
|
float y;
|
|
float rank;
|
|
} par_vec3;
|
|
|
|
typedef struct {
|
|
int n, e, s, w;
|
|
int nsubtiles, nsubdivs, npoints, nsubpts;
|
|
int** subdivs;
|
|
par_vec2* points;
|
|
par_vec2* subpts;
|
|
} par_tile;
|
|
|
|
struct par_bluenoise_context_s {
|
|
par_vec3* points;
|
|
par_tile* tiles;
|
|
float global_density;
|
|
float left, bottom, right, top;
|
|
int ntiles, nsubtiles, nsubdivs;
|
|
int npoints;
|
|
int maxpoints;
|
|
int density_width;
|
|
int density_height;
|
|
unsigned char* density;
|
|
float mag;
|
|
int window_width;
|
|
int window_height;
|
|
int abridged;
|
|
};
|
|
|
|
static float sample_density(par_bluenoise_context* ctx, float x, float y)
|
|
{
|
|
unsigned char* density = ctx->density;
|
|
if (!density) {
|
|
return 1;
|
|
}
|
|
int width = ctx->density_width;
|
|
int height = ctx->density_height;
|
|
y = 1 - y;
|
|
x -= 0.5;
|
|
y -= 0.5;
|
|
float tx = x * PAR_MAXI(width, height);
|
|
float ty = y * PAR_MAXI(width, height);
|
|
x += 0.5;
|
|
y += 0.5;
|
|
tx += width / 2;
|
|
ty += height / 2;
|
|
int ix = PAR_CLAMP((int) tx, 0, width - 2);
|
|
int iy = PAR_CLAMP((int) ty, 0, height - 2);
|
|
return density[iy * width + ix] / 255.0f;
|
|
}
|
|
|
|
static void recurse_tile(
|
|
par_bluenoise_context* ctx, par_tile* tile, float x, float y, int level)
|
|
{
|
|
float left = ctx->left, right = ctx->right;
|
|
float top = ctx->top, bottom = ctx->bottom;
|
|
float mag = ctx->mag;
|
|
float tileSize = 1.f / powf(ctx->nsubtiles, level);
|
|
if (x + tileSize < left || x > right || y + tileSize < bottom || y > top) {
|
|
return;
|
|
}
|
|
float depth = powf(ctx->nsubtiles, 2 * level);
|
|
float threshold = mag / depth * ctx->global_density - tile->npoints;
|
|
int ntests = PAR_MINI(tile->nsubpts, threshold);
|
|
float factor = 1.f / mag * depth / ctx->global_density;
|
|
for (int i = 0; i < ntests; i++) {
|
|
float px = x + tile->subpts[i].x * tileSize;
|
|
float py = y + tile->subpts[i].y * tileSize;
|
|
if (px < left || px > right || py < bottom || py > top) {
|
|
continue;
|
|
}
|
|
if (sample_density(ctx, px, py) < (i + tile->npoints) * factor) {
|
|
continue;
|
|
}
|
|
ctx->points[ctx->npoints].x = px - 0.5;
|
|
ctx->points[ctx->npoints].y = py - 0.5;
|
|
ctx->points[ctx->npoints].rank = (level + 1) + i * factor;
|
|
ctx->npoints++;
|
|
if (ctx->npoints >= ctx->maxpoints) {
|
|
return;
|
|
}
|
|
}
|
|
const float scale = tileSize / ctx->nsubtiles;
|
|
if (threshold <= tile->nsubpts) {
|
|
return;
|
|
}
|
|
level++;
|
|
for (int ty = 0; ty < ctx->nsubtiles; ty++) {
|
|
for (int tx = 0; tx < ctx->nsubtiles; tx++) {
|
|
int tileIndex = tile->subdivs[0][ty * ctx->nsubtiles + tx];
|
|
par_tile* subtile = &ctx->tiles[tileIndex];
|
|
recurse_tile(ctx, subtile, x + tx * scale, y + ty * scale, level);
|
|
}
|
|
}
|
|
}
|
|
|
|
void par_bluenoise_set_window(par_bluenoise_context* ctx, int width, int height)
|
|
{
|
|
ctx->window_width = width;
|
|
ctx->window_height = height;
|
|
}
|
|
|
|
void par_bluenoise_set_viewport(par_bluenoise_context* ctx, float left,
|
|
float bottom, float right, float top)
|
|
{
|
|
// Transform [-.5, +.5] to [0, 1]
|
|
left = ctx->left = left + 0.5;
|
|
right = ctx->right = right + 0.5;
|
|
bottom = ctx->bottom = bottom + 0.5;
|
|
top = ctx->top = top + 0.5;
|
|
|
|
// Determine magnification factor BEFORE clamping.
|
|
float scale = 1000 * (top - bottom) / ctx->window_height;
|
|
ctx->mag = powf(scale, -2);
|
|
|
|
// The density function is only sampled in [0, +1].
|
|
ctx->left = PAR_CLAMP(left, 0, 1);
|
|
ctx->right = PAR_CLAMP(right, 0, 1);
|
|
ctx->bottom = PAR_CLAMP(bottom, 0, 1);
|
|
ctx->top = PAR_CLAMP(top, 0, 1);
|
|
}
|
|
|
|
float* par_bluenoise_generate(
|
|
par_bluenoise_context* ctx, float density, int* npts)
|
|
{
|
|
ctx->global_density = density;
|
|
ctx->npoints = 0;
|
|
float left = ctx->left;
|
|
float right = ctx->right;
|
|
float bottom = ctx->bottom;
|
|
float top = ctx->top;
|
|
float mag = ctx->mag;
|
|
|
|
int ntests = PAR_MINI(ctx->tiles[0].npoints, mag * ctx->global_density);
|
|
float factor = 1.f / mag / ctx->global_density;
|
|
for (int i = 0; i < ntests; i++) {
|
|
float px = ctx->tiles[0].points[i].x;
|
|
float py = ctx->tiles[0].points[i].y;
|
|
if (px < left || px > right || py < bottom || py > top) {
|
|
continue;
|
|
}
|
|
if (sample_density(ctx, px, py) < (i + 1) * factor) {
|
|
continue;
|
|
}
|
|
ctx->points[ctx->npoints].x = px - 0.5;
|
|
ctx->points[ctx->npoints].y = py - 0.5;
|
|
ctx->points[ctx->npoints].rank = i * factor;
|
|
ctx->npoints++;
|
|
if (ctx->npoints >= ctx->maxpoints) {
|
|
break;
|
|
}
|
|
}
|
|
recurse_tile(ctx, &ctx->tiles[0], 0, 0, 0);
|
|
*npts = ctx->npoints;
|
|
return &ctx->points->x;
|
|
}
|
|
|
|
#define freadi() \
|
|
*((int*) ptr); \
|
|
ptr += sizeof(int)
|
|
|
|
#define freadf() \
|
|
*((float*) ptr); \
|
|
ptr += sizeof(float)
|
|
|
|
static par_bluenoise_context* par_bluenoise_create(
|
|
char const* filepath, int nbytes, int maxpts)
|
|
{
|
|
par_bluenoise_context* ctx = PAR_MALLOC(par_bluenoise_context, 1);
|
|
ctx->maxpoints = maxpts;
|
|
ctx->points = PAR_MALLOC(par_vec3, maxpts);
|
|
ctx->density = 0;
|
|
ctx->abridged = 0;
|
|
par_bluenoise_set_window(ctx, 1024, 768);
|
|
par_bluenoise_set_viewport(ctx, -.5, -.5, .5, .5);
|
|
|
|
char* buf = 0;
|
|
if (nbytes == 0) {
|
|
FILE* fin = fopen(filepath, "rb");
|
|
assert(fin);
|
|
fseek(fin, 0, SEEK_END);
|
|
nbytes = (int) ftell(fin);
|
|
fseek(fin, 0, SEEK_SET);
|
|
buf = PAR_MALLOC(char, nbytes);
|
|
int consumed = (int) fread(buf, nbytes, 1, fin);
|
|
assert(consumed == 1);
|
|
fclose(fin);
|
|
}
|
|
|
|
char const* ptr = buf ? buf : filepath;
|
|
int ntiles = ctx->ntiles = freadi();
|
|
int nsubtiles = ctx->nsubtiles = freadi();
|
|
int nsubdivs = ctx->nsubdivs = freadi();
|
|
par_tile* tiles = ctx->tiles = PAR_MALLOC(par_tile, ntiles);
|
|
for (int i = 0; i < ntiles; i++) {
|
|
tiles[i].n = freadi();
|
|
tiles[i].e = freadi();
|
|
tiles[i].s = freadi();
|
|
tiles[i].w = freadi();
|
|
tiles[i].subdivs = PAR_MALLOC(int*, nsubdivs);
|
|
for (int j = 0; j < nsubdivs; j++) {
|
|
int* subdiv = PAR_MALLOC(int, PAR_SQR(nsubtiles));
|
|
for (int k = 0; k < PAR_SQR(nsubtiles); k++) {
|
|
subdiv[k] = freadi();
|
|
}
|
|
tiles[i].subdivs[j] = subdiv;
|
|
}
|
|
tiles[i].npoints = freadi();
|
|
tiles[i].points = PAR_MALLOC(par_vec2, tiles[i].npoints);
|
|
for (int j = 0; j < tiles[i].npoints; j++) {
|
|
tiles[i].points[j].x = freadf();
|
|
tiles[i].points[j].y = freadf();
|
|
}
|
|
tiles[i].nsubpts = freadi();
|
|
tiles[i].subpts = PAR_MALLOC(par_vec2, tiles[i].nsubpts);
|
|
for (int j = 0; j < tiles[i].nsubpts; j++) {
|
|
tiles[i].subpts[j].x = freadf();
|
|
tiles[i].subpts[j].y = freadf();
|
|
}
|
|
|
|
// The following hack allows for an optimization whereby
|
|
// the first tile's point set is re-used for every other tile.
|
|
// This goes against the entire purpose of Recursive Wang Tiles,
|
|
// but in many applications the qualatitive loss is not
|
|
// observable, and the footprint savings are huge (10x).
|
|
|
|
if (tiles[i].npoints == 0) {
|
|
ctx->abridged = 1;
|
|
tiles[i].npoints = tiles[0].npoints;
|
|
tiles[i].points = tiles[0].points;
|
|
tiles[i].nsubpts = tiles[0].nsubpts;
|
|
tiles[i].subpts = tiles[0].subpts;
|
|
}
|
|
}
|
|
free(buf);
|
|
return ctx;
|
|
}
|
|
|
|
par_bluenoise_context* par_bluenoise_from_file(char const* path, int maxpts)
|
|
{
|
|
return par_bluenoise_create(path, 0, maxpts);
|
|
}
|
|
|
|
par_bluenoise_context* par_bluenoise_from_buffer(
|
|
par_byte const* buffer, int nbytes, int maxpts)
|
|
{
|
|
return par_bluenoise_create((char const*) buffer, nbytes, maxpts);
|
|
}
|
|
|
|
void par_bluenoise_density_from_gray(par_bluenoise_context* ctx,
|
|
const unsigned char* pixels, int width, int height, int bpp)
|
|
{
|
|
ctx->density_width = width;
|
|
ctx->density_height = height;
|
|
ctx->density = PAR_MALLOC(unsigned char, width * height);
|
|
unsigned char* dst = ctx->density;
|
|
for (int j = 0; j < height; j++) {
|
|
for (int i = 0; i < width; i++) {
|
|
*dst++ = 255 - (*pixels);
|
|
pixels += bpp;
|
|
}
|
|
}
|
|
}
|
|
|
|
void par_bluenoise_density_from_color(par_bluenoise_context* ctx,
|
|
const unsigned char* pixels, int width, int height, int bpp,
|
|
unsigned int background_color, int invert)
|
|
{
|
|
unsigned int bkgd = background_color;
|
|
ctx->density_width = width;
|
|
ctx->density_height = height;
|
|
ctx->density = PAR_MALLOC(unsigned char, width * height);
|
|
unsigned char* dst = ctx->density;
|
|
unsigned int mask = 0x000000ffu;
|
|
if (bpp > 1) {
|
|
mask |= 0x0000ff00u;
|
|
}
|
|
if (bpp > 2) {
|
|
mask |= 0x00ff0000u;
|
|
}
|
|
if (bpp > 3) {
|
|
mask |= 0xff000000u;
|
|
}
|
|
assert(bpp <= 4);
|
|
for (int j = 0; j < height; j++) {
|
|
for (int i = 0; i < width; i++) {
|
|
unsigned int val = (*((unsigned int*) pixels)) & mask;
|
|
val = invert ? (val == bkgd) : (val != bkgd);
|
|
*dst++ = val ? 255 : 0;
|
|
pixels += bpp;
|
|
}
|
|
}
|
|
}
|
|
|
|
void par_bluenoise_free(par_bluenoise_context* ctx)
|
|
{
|
|
free(ctx->points);
|
|
for (int t = 0; t < ctx->ntiles; t++) {
|
|
for (int s = 0; s < ctx->nsubdivs; s++) {
|
|
free(ctx->tiles[t].subdivs[s]);
|
|
}
|
|
free(ctx->tiles[t].subdivs);
|
|
if (t == 0 || !ctx->abridged) {
|
|
free(ctx->tiles[t].points);
|
|
free(ctx->tiles[t].subpts);
|
|
}
|
|
}
|
|
free(ctx->tiles);
|
|
free(ctx->density);
|
|
}
|
|
|
|
int cmp(const void* a, const void* b)
|
|
{
|
|
const par_vec3* v1 = (const par_vec3*) a;
|
|
const par_vec3* v2 = (const par_vec3*) b;
|
|
if (v1->rank < v2->rank) {
|
|
return -1;
|
|
}
|
|
if (v1->rank > v2->rank) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void par_bluenoise_sort_by_rank(float* floats, int npts)
|
|
{
|
|
par_vec3* vecs = (par_vec3*) floats;
|
|
qsort(vecs, npts, sizeof(vecs[0]), cmp);
|
|
for (int i = 0; i < npts; i++) {
|
|
vecs[i].rank = i;
|
|
}
|
|
}
|
|
|
|
float* par_bluenoise_generate_exact(
|
|
par_bluenoise_context* ctx, int npts, int stride)
|
|
{
|
|
assert(stride >= 2);
|
|
int maxpoints = npts * 2;
|
|
if (ctx->maxpoints < maxpoints) {
|
|
free(ctx->points);
|
|
ctx->maxpoints = maxpoints;
|
|
ctx->points = PAR_MALLOC(par_vec3, maxpoints);
|
|
}
|
|
int ngenerated = 0;
|
|
int nprevious = 0;
|
|
int ndesired = npts;
|
|
float density = 2048;
|
|
while (ngenerated < ndesired) {
|
|
par_bluenoise_generate(ctx, density, &ngenerated);
|
|
|
|
// Might be paranoid, but break if something fishy is going on:
|
|
if (ngenerated == nprevious) {
|
|
return 0;
|
|
}
|
|
|
|
// Perform crazy heuristic to approach a nice density:
|
|
if (ndesired / ngenerated >= 2) {
|
|
density *= 2;
|
|
} else {
|
|
density += density / 10;
|
|
}
|
|
|
|
nprevious = ngenerated;
|
|
}
|
|
par_bluenoise_sort_by_rank(&ctx->points->x, ngenerated);
|
|
if (stride != 3) {
|
|
int nbytes = sizeof(float) * stride * ndesired;
|
|
float* pts = PAR_MALLOC(float, stride * ndesired);
|
|
float* dst = pts;
|
|
const float* src = &ctx->points->x;
|
|
for (int i = 0; i < ndesired; i++, src++) {
|
|
*dst++ = *src++;
|
|
*dst++ = *src++;
|
|
if (stride > 3) {
|
|
*dst++ = *src;
|
|
dst += stride - 3;
|
|
}
|
|
}
|
|
memcpy(ctx->points, pts, nbytes);
|
|
free(pts);
|
|
}
|
|
return &ctx->points->x;
|
|
}
|
|
|
|
#undef PAR_MINI
|
|
#undef PAR_MAXI
|
|
|
|
#endif // PAR_BLUENOISE_IMPLEMENTATION
|
|
#endif // PAR_BLUENOISE_H
|
|
|
|
// par_bluenoise is distributed under the MIT license:
|
|
//
|
|
// Copyright (c) 2019 Philip Rideout
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|