draw2d
This commit is contained in:
697
clay.cm
697
clay.cm
@@ -1,410 +1,353 @@
|
||||
// Layout code
|
||||
// Contain is for how it will treat its children. If they should be laid out as a row, or column, or in a flex style, etc.
|
||||
|
||||
// TODO: needs to remove __proto__
|
||||
// clay2.cm - Revised UI layout engine emitting flat drawables
|
||||
//
|
||||
// Changes from clay.cm:
|
||||
// - No __proto__, uses meme/merge
|
||||
// - Emits flat list of drawables for film2d
|
||||
// - Supports scissor clipping
|
||||
//
|
||||
// Now returns [drawable, drawable, ...] instead of {type:'group', ...}
|
||||
|
||||
var layout = use('layout')
|
||||
var geometry = use('geometry')
|
||||
var draw = use('draw2d')
|
||||
var graphics = use('graphics')
|
||||
var prosperon = use('prosperon')
|
||||
|
||||
var CHILDREN = 'children'
|
||||
var PARENT = 'parent'
|
||||
var clay = {}
|
||||
|
||||
function normalizeSpacing(spacing) {
|
||||
if (is_number(spacing)) {
|
||||
return {l: spacing, r: spacing, t: spacing, b: spacing}
|
||||
} else if (is_array(spacing)) {
|
||||
if (spacing.length == 2) {
|
||||
return {l: spacing[0], r: spacing[0], t: spacing[1], b: spacing[1]}
|
||||
} else if (spacing.length == 4) {
|
||||
return {l: spacing[0], r: spacing[1], t: spacing[2], b: spacing[3]}
|
||||
}
|
||||
} else if (is_object(spacing)) {
|
||||
return {l: spacing.l || 0, r: spacing.r || 0, t: spacing.t || 0, b: spacing.b || 0}
|
||||
} else {
|
||||
return {l:0, r:0, t:0, b:0}
|
||||
}
|
||||
}
|
||||
// Layout context
|
||||
var lay_ctx = layout.make_context()
|
||||
|
||||
var lay_ctx = layout.make_context();
|
||||
|
||||
var clay_base = {
|
||||
// Base configuration for UI elements
|
||||
var base_config = {
|
||||
font: null,
|
||||
background_image: null,
|
||||
slice: 0,
|
||||
font: 'smalle.16',
|
||||
font_size: null,
|
||||
color: {r:1,g:1,b:1,a:1},
|
||||
spacing:0,
|
||||
padding:0,
|
||||
margin:0,
|
||||
offset:{x:0, y:0},
|
||||
size:null,
|
||||
font_path: 'fonts/dos', // Default font
|
||||
font_size: 16,
|
||||
color: {r:1, g:1, b:1, a:1},
|
||||
spacing: 0,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
offset: {x:0, y:0},
|
||||
size: null,
|
||||
background_color: null,
|
||||
clipped: false,
|
||||
text_break: 'word',
|
||||
text_align: 'left',
|
||||
max_size: null, // {width: null, height: null}
|
||||
};
|
||||
|
||||
var root_item;
|
||||
var root_config;
|
||||
var tree_root;
|
||||
var clay = {}
|
||||
clay.CHILDREN = CHILDREN
|
||||
clay.PARENT = PARENT
|
||||
|
||||
var focused_textbox = null
|
||||
|
||||
clay.behave = layout.behave;
|
||||
clay.contain = layout.contain;
|
||||
|
||||
clay.draw = function draw(fn, size = [prosperon.camera.width, prosperon.camera.height])
|
||||
{
|
||||
lay_ctx.reset();
|
||||
var root = lay_ctx.item();
|
||||
// Accept both array and object formats
|
||||
if (is_array(size)) {
|
||||
size = {width: size[0], height: size[1]};
|
||||
}
|
||||
lay_ctx.set_size(root,size);
|
||||
lay_ctx.set_contain(root,layout.contain.row);
|
||||
root_item = root;
|
||||
root_config = object(clay_base);
|
||||
tree_root = {
|
||||
id: root,
|
||||
config: root_config,
|
||||
};
|
||||
tree_root[CHILDREN] = [];
|
||||
tree_root[PARENT] = null;
|
||||
fn()
|
||||
lay_ctx.run();
|
||||
|
||||
// Adjust bounding boxes for padding - traverse tree instead of array
|
||||
function adjust_bounding_boxes(node) {
|
||||
node.content = lay_ctx.get_rect(node.id);
|
||||
node.boundingbox = object(node.content);
|
||||
|
||||
var padding = normalizeSpacing(node.config.padding || 0);
|
||||
|
||||
node.boundingbox.x -= padding.l;
|
||||
node.boundingbox.y -= padding.t;
|
||||
node.boundingbox.width += padding.l + padding.r;
|
||||
node.boundingbox.height += padding.t + padding.b;
|
||||
|
||||
node.marginbox = object(node.content);
|
||||
var margin = normalizeSpacing(node.config.margin || 0);
|
||||
node.marginbox.x -= margin.l;
|
||||
node.marginbox.y -= margin.t;
|
||||
node.marginbox.width += margin.l+margin.r;
|
||||
node.marginbox.height += margin.t+margin.b;
|
||||
|
||||
// Apply max_size clamping post-layout
|
||||
if (node.config.max_size) {
|
||||
if (node.config.max_size.width != null) {
|
||||
// Clamp the layout rect size
|
||||
var rect = lay_ctx.get_rect(node.id);
|
||||
rect.width = number.min(rect.width, node.config.max_size.width);
|
||||
// Also clamp bounding box
|
||||
node.content.width = number.min(node.content.width, node.config.max_size.width);
|
||||
node.boundingbox.width = number.min(node.boundingbox.width, node.config.max_size.width + padding.l + padding.r);
|
||||
node.marginbox.width = number.min(node.marginbox.width, node.config.max_size.width + padding.l + padding.r + margin.l + margin.r);
|
||||
}
|
||||
if (node.config.max_size.height != null) {
|
||||
// Clamp the layout rect size
|
||||
var rect = lay_ctx.get_rect(node.id);
|
||||
rect.height = number.min(rect.height, node.config.max_size.height);
|
||||
// Also clamp bounding box
|
||||
node.content.height = number.min(node.content.height, node.config.max_size.height);
|
||||
node.boundingbox.height = number.min(node.boundingbox.height, node.config.max_size.height + padding.t + padding.b);
|
||||
node.marginbox.height = number.min(node.marginbox.height, node.config.max_size.height + padding.t + padding.b + margin.t + margin.b);
|
||||
}
|
||||
}
|
||||
|
||||
node.content.y *= -1;
|
||||
node.content.y += size.height;
|
||||
node.boundingbox.y *= -1;
|
||||
node.boundingbox.y += size.height;
|
||||
node.content.anchor_y = 1;
|
||||
node.boundingbox.anchor_y = 1;
|
||||
|
||||
// Recursively adjust children
|
||||
if (node[CHILDREN]) {
|
||||
for (var child of node[CHILDREN]) {
|
||||
adjust_bounding_boxes(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adjust_bounding_boxes(tree_root);
|
||||
|
||||
return tree_root;
|
||||
max_size: null,
|
||||
contain: 0,
|
||||
behave: 0
|
||||
}
|
||||
|
||||
function create_view_fn(base_config)
|
||||
{
|
||||
var base = meme(clay_base, base_config)
|
||||
return function view(config = {}, fn) {
|
||||
config.__proto__ = base;
|
||||
var item = add_item(config);
|
||||
function normalize_color(c, fallback) {
|
||||
fallback = fallback || {r:1, g:1, b:1, a:1}
|
||||
if (!c) return {r:fallback.r, g:fallback.g, b:fallback.b, a:fallback.a}
|
||||
return {
|
||||
r: c.r != null ? c.r : fallback.r,
|
||||
g: c.g != null ? c.g : fallback.g,
|
||||
b: c.b != null ? c.b : fallback.b,
|
||||
a: c.a != null ? c.a : fallback.a
|
||||
}
|
||||
}
|
||||
|
||||
var prev_item = root_item;
|
||||
var prev_config = root_config;
|
||||
var prev_tree_root = tree_root;
|
||||
root_item = item;
|
||||
root_config = config;
|
||||
root_config._childIndex = 0; // Initialize child index
|
||||
// Find the tree node for this item and set it as current tree_root
|
||||
if (prev_tree_root[CHILDREN] && prev_tree_root[CHILDREN].length > 0) {
|
||||
tree_root = prev_tree_root[CHILDREN][prev_tree_root[CHILDREN].length - 1];
|
||||
function normalize_spacing(s) {
|
||||
if (is_number(s)) return {l:s, r:s, t:s, b:s}
|
||||
if (is_array(s)) {
|
||||
if (s.length == 2) return {l:s[0], r:s[0], t:s[1], b:s[1]}
|
||||
if (s.length == 4) return {l:s[0], r:s[1], t:s[2], b:s[3]}
|
||||
}
|
||||
if (is_object(s)) return {l:s.l||0, r:s.r||0, t:s.t||0, b:s.b||0}
|
||||
return {l:0, r:0, t:0, b:0}
|
||||
}
|
||||
|
||||
// Tree building state
|
||||
var root_item
|
||||
var tree_root
|
||||
var config_stack = []
|
||||
|
||||
// Rewriting state management for cleaner recursion
|
||||
var tree_stack = []
|
||||
|
||||
clay.layout = function(fn, size) {
|
||||
lay_ctx.reset()
|
||||
var root_id = lay_ctx.item()
|
||||
|
||||
lay_ctx.set_size(root_id, size)
|
||||
lay_ctx.set_contain(root_id, layout.contain.row)
|
||||
|
||||
var root_node = {
|
||||
id: root_id,
|
||||
config: meme(base_config, {size: size}),
|
||||
children: []
|
||||
}
|
||||
|
||||
tree_stack = [root_node]
|
||||
|
||||
fn() // User builds tree
|
||||
|
||||
lay_ctx.run()
|
||||
|
||||
// Post-layout: build flat drawable list
|
||||
return build_drawables(root_node, size.height)
|
||||
}
|
||||
|
||||
function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_scissor, parent_layer) {
|
||||
parent_abs_x = parent_abs_x || 0
|
||||
parent_abs_y = parent_abs_y || 0
|
||||
parent_layer = parent_layer || 0 // UI usually on top, but let's start at 0
|
||||
|
||||
var rect = lay_ctx.get_rect(node.id)
|
||||
|
||||
// Calculate absolute world Y for this node (bottom-up layout to top-down render)
|
||||
var abs_y = root_height - (rect.y + rect.height)
|
||||
var abs_x = rect.x
|
||||
|
||||
// IMPORTANT: The offset in config is applied VISUALLY.
|
||||
var vis_x = abs_x + node.config.offset.x
|
||||
var vis_y = abs_y + node.config.offset.y
|
||||
|
||||
var drawables = []
|
||||
|
||||
// Scissor
|
||||
var current_scissor = parent_scissor
|
||||
if (node.config.clipped) {
|
||||
var sx = vis_x
|
||||
var sy = vis_y
|
||||
var sw = rect.width
|
||||
var sh = rect.height
|
||||
|
||||
// Intersect with parent
|
||||
if (parent_scissor) {
|
||||
sx = number.max(sx, parent_scissor.x)
|
||||
sy = number.max(sy, parent_scissor.y)
|
||||
var right = number.min(vis_x + sw, parent_scissor.x + parent_scissor.width)
|
||||
var bottom = number.min(vis_y + sh, parent_scissor.y + parent_scissor.height)
|
||||
sw = number.max(0, right - sx)
|
||||
sh = number.max(0, bottom - sy)
|
||||
}
|
||||
|
||||
current_scissor = {x: sx, y: sy, width: sw, height: sh}
|
||||
}
|
||||
|
||||
// Background
|
||||
if (node.config.background_image) {
|
||||
if (node.config.slice) {
|
||||
drawables.push({
|
||||
type: 'sprite',
|
||||
image: node.config.background_image,
|
||||
pos: {x: vis_x, y: vis_y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
slice: node.config.slice,
|
||||
color: node.config.background_color || {r:1, g:1, b:1, a:1},
|
||||
layer: parent_layer - 0.1, // slightly behind content
|
||||
scissor: current_scissor
|
||||
})
|
||||
} else {
|
||||
// If no children yet, this shouldn't happen, but handle gracefully
|
||||
tree_root = prev_tree_root;
|
||||
drawables.push({
|
||||
type: 'sprite',
|
||||
image: node.config.background_image,
|
||||
pos: {x: vis_x, y: vis_y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
color: node.config.background_color || {r:1, g:1, b:1, a:1},
|
||||
layer: parent_layer - 0.1,
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
fn?.();
|
||||
root_item = prev_item;
|
||||
root_config = prev_config;
|
||||
tree_root = prev_tree_root;
|
||||
} else if (node.config.background_color) {
|
||||
drawables.push({
|
||||
type: 'rect',
|
||||
pos: {x: vis_x, y: vis_y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
color: node.config.background_color,
|
||||
layer: parent_layer - 0.1,
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
|
||||
// Content (Image/Text)
|
||||
if (node.config.image) {
|
||||
drawables.push({
|
||||
type: 'sprite',
|
||||
image: node.config.image,
|
||||
pos: {x: vis_x, y: vis_y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
color: node.config.color,
|
||||
layer: parent_layer,
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
|
||||
if (node.config.text) {
|
||||
drawables.push({
|
||||
type: 'text',
|
||||
text: node.config.text,
|
||||
font: node.config.font_path,
|
||||
size: node.config.font_size,
|
||||
color: node.config.color,
|
||||
pos: {x: vis_x, y: vis_y + rect.height}, // Baseline adjustment
|
||||
anchor_y: 1.0, // Text usually draws from baseline up or top down?
|
||||
// film2d text uses top-left by default unless anchor set.
|
||||
// Original clay put it at `y + rect.height`.
|
||||
// Let's assume origin top-left, so we might need anchor adjustment or just position.
|
||||
// If frame is top-down (0 at top), `abs_y` is top.
|
||||
// `rect.y` in layout is bottom-up? "rect.y is from bottom" says original comment.
|
||||
// `abs_y = root_height - (rect.y + rect.height)` -> Top edge of element.
|
||||
// Text usually wants baseline.
|
||||
// If we put it at `vis_y + rect.height`, that's bottom of element.
|
||||
layer: parent_layer,
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
clay.vstack = create_view_fn({
|
||||
contain: layout.contain.column | layout.contain.start,
|
||||
});
|
||||
|
||||
clay.hstack = create_view_fn({
|
||||
contain: layout.contain.row | layout.contain.start,
|
||||
});
|
||||
|
||||
clay.spacer = create_view_fn({
|
||||
behave: layout.behave.hfill | layout.behave.vfill
|
||||
});
|
||||
|
||||
clay.frame = create_view_fn({});
|
||||
|
||||
function image_size(img)
|
||||
{
|
||||
return [img.width * (img.rect?.width || 1), img.height * (img.rect?.height || 1)];
|
||||
}
|
||||
|
||||
function add_item(config)
|
||||
{
|
||||
// Normalize the child's margin
|
||||
var margin = normalizeSpacing(config.margin || 0);
|
||||
var padding = normalizeSpacing(config.padding || 0);
|
||||
var childGap = root_config.child_gap || 0;
|
||||
|
||||
// Adjust for child_gap
|
||||
root_config._childIndex ??= 0
|
||||
if (root_config._childIndex > 0) {
|
||||
var parentContain = root_config.contain || 0;
|
||||
var directionMask = layout.contain.row | layout.contain.column;
|
||||
var direction = parentContain & directionMask;
|
||||
var isVStack = direction == layout.contain.column;
|
||||
var isHStack = direction == layout.contain.row;
|
||||
|
||||
if (isVStack) {
|
||||
margin.t += childGap;
|
||||
} else if (isHStack) {
|
||||
margin.l += childGap;
|
||||
// Children
|
||||
for (var child of node.children) {
|
||||
var child_drawables = build_drawables(child, root_height, vis_x, vis_y, current_scissor, parent_layer + 0.01)
|
||||
for (var i = 0; i < child_drawables.length; i++) {
|
||||
drawables.push(child_drawables[i])
|
||||
}
|
||||
}
|
||||
|
||||
return drawables
|
||||
}
|
||||
|
||||
var use_config = meme(config);
|
||||
|
||||
use_config.margin = {
|
||||
t: margin.t+padding.t,
|
||||
b: margin.b+padding.b,
|
||||
r:margin.r+padding.r,
|
||||
l:margin.l+padding.l
|
||||
};
|
||||
// --- Item Creation Helpers ---
|
||||
|
||||
var item = lay_ctx.item();
|
||||
lay_ctx.set_margins(item, use_config.margin);
|
||||
use_config.size ??= {width:0, height:0}
|
||||
// Convert array to object if needed
|
||||
if (is_array(use_config.size)) {
|
||||
use_config.size = {width: use_config.size[0], height: use_config.size[1]};
|
||||
function process_configs(configs) {
|
||||
var cfg = meme(base_config, ...configs)
|
||||
|
||||
cfg.color = normalize_color(cfg.color, base_config.color)
|
||||
if (cfg.background_color) cfg.background_color = normalize_color(cfg.background_color, {r:1,g:1,b:1,a:1})
|
||||
|
||||
if (!cfg.offset) cfg.offset = {x:0, y:0}
|
||||
else cfg.offset = {x: cfg.offset.x || 0, y: cfg.offset.y || 0}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
function push_node(configs, contain_mode) {
|
||||
var config = process_configs(configs)
|
||||
if (contain_mode != null) config.contain = contain_mode
|
||||
|
||||
var item = lay_ctx.item()
|
||||
|
||||
// Apply layout props
|
||||
lay_ctx.set_margins(item, normalize_spacing(config.margin))
|
||||
lay_ctx.set_contain(item, config.contain)
|
||||
lay_ctx.set_behave(item, config.behave)
|
||||
|
||||
if (config.size) {
|
||||
var s = config.size
|
||||
if (is_array(s)) s = {width: s[0], height: s[1]}
|
||||
lay_ctx.set_size(item, s)
|
||||
}
|
||||
// Apply max_size constraint - only clamp computed size, don't set explicit size pre-layout
|
||||
if (use_config.max_size) {
|
||||
// max_size should not force explicit sizing pre-layout - let children compute natural size,
|
||||
// then clamp the container after layout. For now, just ensure we don't set size to max_size.
|
||||
}
|
||||
lay_ctx.set_size(item,use_config.size);
|
||||
lay_ctx.set_contain(item,use_config.contain);
|
||||
lay_ctx.set_behave(item,use_config.behave);
|
||||
var tree_node = {
|
||||
|
||||
var node = {
|
||||
id: item,
|
||||
config: use_config,
|
||||
};
|
||||
tree_node[CHILDREN] = [];
|
||||
tree_node[PARENT] = tree_root;
|
||||
tree_root[CHILDREN].push(tree_node);
|
||||
lay_ctx.insert(root_item,item);
|
||||
|
||||
// Increment the parent's child index
|
||||
root_config._childIndex++;
|
||||
return item;
|
||||
}
|
||||
|
||||
function rectify_configs(config_array)
|
||||
{
|
||||
if (config_array.length == 0)
|
||||
config_array = [{}];
|
||||
|
||||
for (var i = config_array.length-1; i > 0; i--)
|
||||
config_array[i].__proto__ = config_array[i-1];
|
||||
|
||||
config_array[0].__proto__ = clay_base;
|
||||
var cleanobj = meme(config_array[config_array.length-1]);
|
||||
|
||||
return cleanobj;
|
||||
}
|
||||
|
||||
clay.image = function image(path, ...configs)
|
||||
{
|
||||
var config = rectify_configs(configs);
|
||||
var image = graphics.texture(path);
|
||||
config.image = path; // Store the path string, not the texture object
|
||||
config.size ??= {width: image.width, height: image.height};
|
||||
add_item(config);
|
||||
}
|
||||
|
||||
clay.text = function text(str, ...configs)
|
||||
{
|
||||
var config = rectify_configs(configs);
|
||||
config.size ??= [0,0]
|
||||
config.font = graphics.get_font(config.font)
|
||||
config.text = str
|
||||
var tsize = config.font.text_size(str, 0, config.size[0], config.text_break, config.text_align);
|
||||
tsize.x = number.ceiling(tsize.x)
|
||||
tsize.y = number.ceiling(tsize.y)
|
||||
config.size = config.size.map((x,i) => number.max(x, tsize[i]));
|
||||
add_item(config);
|
||||
}
|
||||
|
||||
/*
|
||||
For a given size,
|
||||
the layout engine should "see" size + margin
|
||||
but its interior content should "see" size - padding
|
||||
hence, the layout box should be size-padding, with margin of margin+padding
|
||||
*/
|
||||
|
||||
var button_base = meme(clay_base, {
|
||||
padding:0,
|
||||
hovered:{
|
||||
}
|
||||
});
|
||||
clay.button = function button(str, action, config = {})
|
||||
{
|
||||
config.__proto__ = button_base;
|
||||
config.font = graphics.get_font(config.font)
|
||||
config.size = config.font.text_size(str, 0, 0, config.text_break, config.text_align)
|
||||
add_item(config);
|
||||
config.text = str;
|
||||
config.action = action;
|
||||
}
|
||||
|
||||
var point = use('point')
|
||||
|
||||
clay.draw_commands = function draw_commands(tree_root, pos = {x:0,y:0})
|
||||
{
|
||||
function draw_node(node) {
|
||||
var config = node.config
|
||||
var boundingbox = geometry.rect_move(node.boundingbox,point.add(pos,config.offset))
|
||||
var content = geometry.rect_move(node.content,point.add(pos, config.offset))
|
||||
|
||||
if (config.background_image)
|
||||
if (config.slice)
|
||||
draw.slice9(config.background_image, boundingbox, config.slice, config.background_color)
|
||||
else
|
||||
draw.image(config.background_image, boundingbox, 0, config.color)
|
||||
else if (config.background_color)
|
||||
draw.rectangle(boundingbox, null, {color:config.background_color})
|
||||
|
||||
if (config.text) {
|
||||
var baseline_y = content.y + content.height - config.font.ascent
|
||||
draw.text(config.text, {x: content.x, y: baseline_y}, config.font, config.color, content.width)
|
||||
}
|
||||
|
||||
if (config.image)
|
||||
draw.image(config.image, content, 0, config.color)
|
||||
|
||||
if (config.clipped) {
|
||||
draw.scissor(content)
|
||||
}
|
||||
|
||||
// Recursively draw children
|
||||
if (node[CHILDREN]) {
|
||||
for (var child of node[CHILDREN]) {
|
||||
draw_node(child);
|
||||
}
|
||||
}
|
||||
if (config.clipped)
|
||||
draw.scissor(null)
|
||||
}
|
||||
|
||||
draw_node(tree_root);
|
||||
}
|
||||
|
||||
var dbg_colors = {};
|
||||
clay.debug_colors = dbg_colors;
|
||||
dbg_colors.content = [1,0,0,0.1];
|
||||
dbg_colors.boundingbox = [0,1,0,0,0.1];
|
||||
dbg_colors.margin = [0,0,1,0.1];
|
||||
clay.draw_debug = function draw_debug(tree_root, pos = {x:0, y:0})
|
||||
{
|
||||
function draw_debug_node(node) {
|
||||
var boundingbox = geometry.rect_move(node.boundingbox,pos);
|
||||
var content = geometry.rect_move(node.content,pos);
|
||||
draw.rectangle(content, null, {color:dbg_colors.content});
|
||||
draw.rectangle(boundingbox, null, {color:dbg_colors.boundingbox});
|
||||
// draw.rectangle(geometry.rect_move(node.marginbox,pos), dbg_colors.margin);
|
||||
|
||||
// Recursively draw debug for children
|
||||
if (node[CHILDREN]) {
|
||||
for (var child of node[CHILDREN]) {
|
||||
draw_debug_node(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw_debug_node(tree_root);
|
||||
}
|
||||
|
||||
clay.print_tree = function print_tree(tree_root, indent = 0) {
|
||||
var indent_str = ' '.repeat(indent)
|
||||
var node_type = 'unknown'
|
||||
|
||||
if (tree_root.config.text) {
|
||||
node_type = 'text'
|
||||
} else if (tree_root.config.image) {
|
||||
node_type = 'image'
|
||||
} else if (tree_root.config.contain) {
|
||||
if (tree_root.config.contain & layout.contain.column) {
|
||||
node_type = 'vstack'
|
||||
} else if (tree_root.config.contain & layout.contain.row) {
|
||||
node_type = 'hstack'
|
||||
} else {
|
||||
node_type = 'container'
|
||||
}
|
||||
} else {
|
||||
node_type = 'node'
|
||||
config: config,
|
||||
children: []
|
||||
}
|
||||
|
||||
log.console(`${indent_str}${node_type} (id: ${tree_root.id})`)
|
||||
// Add to parent
|
||||
var parent = tree_stack[tree_stack.length-1]
|
||||
parent.children.push(node)
|
||||
lay_ctx.insert(parent.id, item)
|
||||
|
||||
if (tree_root[CHILDREN] && tree_root[CHILDREN].length > 0) {
|
||||
log.console(`${indent_str} children: ${tree_root[CHILDREN].length}`)
|
||||
for (var child of tree_root[CHILDREN]) {
|
||||
print_tree(child, indent + 1)
|
||||
}
|
||||
} else {
|
||||
log.console(`${indent_str} (no children)`)
|
||||
}
|
||||
tree_stack.push(node)
|
||||
return node
|
||||
}
|
||||
|
||||
function pop_node() {
|
||||
tree_stack.pop()
|
||||
}
|
||||
|
||||
// Generic container
|
||||
clay.container = function(configs, fn) {
|
||||
if (is_function(configs)) { fn = configs; configs = {} }
|
||||
if (!is_array(configs)) configs = [configs]
|
||||
|
||||
push_node(configs, null)
|
||||
if (fn) fn()
|
||||
pop_node()
|
||||
}
|
||||
|
||||
// Stacks
|
||||
clay.vstack = function(configs, fn) {
|
||||
if (is_function(configs)) { fn = configs; configs = {} }
|
||||
if (!is_array(configs)) configs = [configs]
|
||||
|
||||
var c = layout.contain.column
|
||||
|
||||
push_node(configs, c)
|
||||
if (fn) fn()
|
||||
pop_node()
|
||||
}
|
||||
|
||||
clay.hstack = function(configs, fn) {
|
||||
if (is_function(configs)) { fn = configs; configs = {} }
|
||||
if (!is_array(configs)) configs = [configs]
|
||||
|
||||
var c = layout.contain.row
|
||||
push_node(configs, c)
|
||||
if (fn) fn()
|
||||
pop_node()
|
||||
}
|
||||
|
||||
clay.zstack = function(configs, fn) {
|
||||
if (is_function(configs)) { fn = configs; configs = {} }
|
||||
if (!is_array(configs)) configs = [configs]
|
||||
|
||||
var c = layout.contain.layout
|
||||
|
||||
push_node(configs, c)
|
||||
if (fn) fn()
|
||||
pop_node()
|
||||
}
|
||||
|
||||
// Leaf nodes
|
||||
clay.image = function(path, ...configs) {
|
||||
var img = graphics.texture(path)
|
||||
var c = {image: path}
|
||||
var final_config = process_configs(configs)
|
||||
if (!final_config.size && !final_config.behave) {
|
||||
c.size = {width: img.width, height: img.height}
|
||||
}
|
||||
|
||||
push_node([c, ...configs], null)
|
||||
pop_node()
|
||||
}
|
||||
|
||||
clay.text = function(str, ...configs) {
|
||||
var c = {text: str}
|
||||
var final_config = process_configs(configs)
|
||||
if (!final_config.size && !final_config.behave) {
|
||||
c.size = {width: 100, height: 20}
|
||||
}
|
||||
|
||||
push_node([c, ...configs], null)
|
||||
pop_node()
|
||||
}
|
||||
|
||||
clay.rectangle = function(...configs) {
|
||||
push_node(configs, null)
|
||||
pop_node()
|
||||
}
|
||||
|
||||
clay.button = function(str, action, ...configs) {
|
||||
var btn_config = {
|
||||
padding: 10,
|
||||
background_color: {r:0.3, g:0.3, b:0.4, a:1}
|
||||
}
|
||||
|
||||
clay.zstack([btn_config, ...configs], function() {
|
||||
clay.text(str, {color: {r:1,g:1,b:1,a:1}})
|
||||
})
|
||||
}
|
||||
|
||||
// Constants
|
||||
clay.behave = layout.behave
|
||||
clay.contain = layout.contain
|
||||
|
||||
return clay
|
||||
|
||||
353
clay2.cm
353
clay2.cm
@@ -1,353 +0,0 @@
|
||||
// clay2.cm - Revised UI layout engine emitting flat drawables
|
||||
//
|
||||
// Changes from clay.cm:
|
||||
// - No __proto__, uses meme/merge
|
||||
// - Emits flat list of drawables for film2d
|
||||
// - Supports scissor clipping
|
||||
//
|
||||
// Now returns [drawable, drawable, ...] instead of {type:'group', ...}
|
||||
|
||||
var layout = use('layout')
|
||||
var graphics = use('graphics')
|
||||
|
||||
var clay = {}
|
||||
|
||||
// Layout context
|
||||
var lay_ctx = layout.make_context()
|
||||
|
||||
// Base configuration for UI elements
|
||||
var base_config = {
|
||||
font: null,
|
||||
background_image: null,
|
||||
slice: 0,
|
||||
font_path: 'fonts/dos', // Default font
|
||||
font_size: 16,
|
||||
color: {r:1, g:1, b:1, a:1},
|
||||
spacing: 0,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
offset: {x:0, y:0},
|
||||
size: null,
|
||||
background_color: null,
|
||||
clipped: false,
|
||||
text_break: 'word',
|
||||
text_align: 'left',
|
||||
max_size: null,
|
||||
contain: 0,
|
||||
behave: 0
|
||||
}
|
||||
|
||||
function normalize_color(c, fallback) {
|
||||
fallback = fallback || {r:1, g:1, b:1, a:1}
|
||||
if (!c) return {r:fallback.r, g:fallback.g, b:fallback.b, a:fallback.a}
|
||||
return {
|
||||
r: c.r != null ? c.r : fallback.r,
|
||||
g: c.g != null ? c.g : fallback.g,
|
||||
b: c.b != null ? c.b : fallback.b,
|
||||
a: c.a != null ? c.a : fallback.a
|
||||
}
|
||||
}
|
||||
|
||||
function normalize_spacing(s) {
|
||||
if (is_number(s)) return {l:s, r:s, t:s, b:s}
|
||||
if (is_array(s)) {
|
||||
if (s.length == 2) return {l:s[0], r:s[0], t:s[1], b:s[1]}
|
||||
if (s.length == 4) return {l:s[0], r:s[1], t:s[2], b:s[3]}
|
||||
}
|
||||
if (is_object(s)) return {l:s.l||0, r:s.r||0, t:s.t||0, b:s.b||0}
|
||||
return {l:0, r:0, t:0, b:0}
|
||||
}
|
||||
|
||||
// Tree building state
|
||||
var root_item
|
||||
var tree_root
|
||||
var config_stack = []
|
||||
|
||||
// Rewriting state management for cleaner recursion
|
||||
var tree_stack = []
|
||||
|
||||
clay.layout = function(fn, size) {
|
||||
lay_ctx.reset()
|
||||
var root_id = lay_ctx.item()
|
||||
|
||||
lay_ctx.set_size(root_id, size)
|
||||
lay_ctx.set_contain(root_id, layout.contain.row)
|
||||
|
||||
var root_node = {
|
||||
id: root_id,
|
||||
config: meme(base_config, {size: size}),
|
||||
children: []
|
||||
}
|
||||
|
||||
tree_stack = [root_node]
|
||||
|
||||
fn() // User builds tree
|
||||
|
||||
lay_ctx.run()
|
||||
|
||||
// Post-layout: build flat drawable list
|
||||
return build_drawables(root_node, size.height)
|
||||
}
|
||||
|
||||
function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_scissor, parent_layer) {
|
||||
parent_abs_x = parent_abs_x || 0
|
||||
parent_abs_y = parent_abs_y || 0
|
||||
parent_layer = parent_layer || 0 // UI usually on top, but let's start at 0
|
||||
|
||||
var rect = lay_ctx.get_rect(node.id)
|
||||
|
||||
// Calculate absolute world Y for this node (bottom-up layout to top-down render)
|
||||
var abs_y = root_height - (rect.y + rect.height)
|
||||
var abs_x = rect.x
|
||||
|
||||
// IMPORTANT: The offset in config is applied VISUALLY.
|
||||
var vis_x = abs_x + node.config.offset.x
|
||||
var vis_y = abs_y + node.config.offset.y
|
||||
|
||||
var drawables = []
|
||||
|
||||
// Scissor
|
||||
var current_scissor = parent_scissor
|
||||
if (node.config.clipped) {
|
||||
var sx = vis_x
|
||||
var sy = vis_y
|
||||
var sw = rect.width
|
||||
var sh = rect.height
|
||||
|
||||
// Intersect with parent
|
||||
if (parent_scissor) {
|
||||
sx = number.max(sx, parent_scissor.x)
|
||||
sy = number.max(sy, parent_scissor.y)
|
||||
var right = number.min(vis_x + sw, parent_scissor.x + parent_scissor.width)
|
||||
var bottom = number.min(vis_y + sh, parent_scissor.y + parent_scissor.height)
|
||||
sw = number.max(0, right - sx)
|
||||
sh = number.max(0, bottom - sy)
|
||||
}
|
||||
|
||||
current_scissor = {x: sx, y: sy, width: sw, height: sh}
|
||||
}
|
||||
|
||||
// Background
|
||||
if (node.config.background_image) {
|
||||
if (node.config.slice) {
|
||||
drawables.push({
|
||||
type: 'sprite',
|
||||
image: node.config.background_image,
|
||||
pos: {x: vis_x, y: vis_y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
slice: node.config.slice,
|
||||
color: node.config.background_color || {r:1, g:1, b:1, a:1},
|
||||
layer: parent_layer - 0.1, // slightly behind content
|
||||
scissor: current_scissor
|
||||
})
|
||||
} else {
|
||||
drawables.push({
|
||||
type: 'sprite',
|
||||
image: node.config.background_image,
|
||||
pos: {x: vis_x, y: vis_y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
color: node.config.background_color || {r:1, g:1, b:1, a:1},
|
||||
layer: parent_layer - 0.1,
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
} else if (node.config.background_color) {
|
||||
drawables.push({
|
||||
type: 'rect',
|
||||
pos: {x: vis_x, y: vis_y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
color: node.config.background_color,
|
||||
layer: parent_layer - 0.1,
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
|
||||
// Content (Image/Text)
|
||||
if (node.config.image) {
|
||||
drawables.push({
|
||||
type: 'sprite',
|
||||
image: node.config.image,
|
||||
pos: {x: vis_x, y: vis_y},
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
color: node.config.color,
|
||||
layer: parent_layer,
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
|
||||
if (node.config.text) {
|
||||
drawables.push({
|
||||
type: 'text',
|
||||
text: node.config.text,
|
||||
font: node.config.font_path,
|
||||
size: node.config.font_size,
|
||||
color: node.config.color,
|
||||
pos: {x: vis_x, y: vis_y + rect.height}, // Baseline adjustment
|
||||
anchor_y: 1.0, // Text usually draws from baseline up or top down?
|
||||
// film2d text uses top-left by default unless anchor set.
|
||||
// Original clay put it at `y + rect.height`.
|
||||
// Let's assume origin top-left, so we might need anchor adjustment or just position.
|
||||
// If frame is top-down (0 at top), `abs_y` is top.
|
||||
// `rect.y` in layout is bottom-up? "rect.y is from bottom" says original comment.
|
||||
// `abs_y = root_height - (rect.y + rect.height)` -> Top edge of element.
|
||||
// Text usually wants baseline.
|
||||
// If we put it at `vis_y + rect.height`, that's bottom of element.
|
||||
layer: parent_layer,
|
||||
scissor: current_scissor
|
||||
})
|
||||
}
|
||||
|
||||
// Children
|
||||
for (var child of node.children) {
|
||||
var child_drawables = build_drawables(child, root_height, vis_x, vis_y, current_scissor, parent_layer + 0.01)
|
||||
for (var i = 0; i < child_drawables.length; i++) {
|
||||
drawables.push(child_drawables[i])
|
||||
}
|
||||
}
|
||||
|
||||
return drawables
|
||||
}
|
||||
|
||||
|
||||
// --- Item Creation Helpers ---
|
||||
|
||||
function process_configs(configs) {
|
||||
var cfg = meme(base_config, ...configs)
|
||||
|
||||
cfg.color = normalize_color(cfg.color, base_config.color)
|
||||
if (cfg.background_color) cfg.background_color = normalize_color(cfg.background_color, {r:1,g:1,b:1,a:1})
|
||||
|
||||
if (!cfg.offset) cfg.offset = {x:0, y:0}
|
||||
else cfg.offset = {x: cfg.offset.x || 0, y: cfg.offset.y || 0}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
function push_node(configs, contain_mode) {
|
||||
var config = process_configs(configs)
|
||||
if (contain_mode != null) config.contain = contain_mode
|
||||
|
||||
var item = lay_ctx.item()
|
||||
|
||||
// Apply layout props
|
||||
lay_ctx.set_margins(item, normalize_spacing(config.margin))
|
||||
lay_ctx.set_contain(item, config.contain)
|
||||
lay_ctx.set_behave(item, config.behave)
|
||||
|
||||
if (config.size) {
|
||||
var s = config.size
|
||||
if (is_array(s)) s = {width: s[0], height: s[1]}
|
||||
lay_ctx.set_size(item, s)
|
||||
}
|
||||
|
||||
var node = {
|
||||
id: item,
|
||||
config: config,
|
||||
children: []
|
||||
}
|
||||
|
||||
// Add to parent
|
||||
var parent = tree_stack[tree_stack.length-1]
|
||||
parent.children.push(node)
|
||||
lay_ctx.insert(parent.id, item)
|
||||
|
||||
tree_stack.push(node)
|
||||
return node
|
||||
}
|
||||
|
||||
function pop_node() {
|
||||
tree_stack.pop()
|
||||
}
|
||||
|
||||
// Generic container
|
||||
clay.container = function(configs, fn) {
|
||||
if (is_function(configs)) { fn = configs; configs = {} }
|
||||
if (!is_array(configs)) configs = [configs]
|
||||
|
||||
push_node(configs, null)
|
||||
if (fn) fn()
|
||||
pop_node()
|
||||
}
|
||||
|
||||
// Stacks
|
||||
clay.vstack = function(configs, fn) {
|
||||
if (is_function(configs)) { fn = configs; configs = {} }
|
||||
if (!is_array(configs)) configs = [configs]
|
||||
|
||||
var c = layout.contain.column
|
||||
|
||||
push_node(configs, c)
|
||||
if (fn) fn()
|
||||
pop_node()
|
||||
}
|
||||
|
||||
clay.hstack = function(configs, fn) {
|
||||
if (is_function(configs)) { fn = configs; configs = {} }
|
||||
if (!is_array(configs)) configs = [configs]
|
||||
|
||||
var c = layout.contain.row
|
||||
push_node(configs, c)
|
||||
if (fn) fn()
|
||||
pop_node()
|
||||
}
|
||||
|
||||
clay.zstack = function(configs, fn) {
|
||||
if (is_function(configs)) { fn = configs; configs = {} }
|
||||
if (!is_array(configs)) configs = [configs]
|
||||
|
||||
var c = layout.contain.layout
|
||||
|
||||
push_node(configs, c)
|
||||
if (fn) fn()
|
||||
pop_node()
|
||||
}
|
||||
|
||||
// Leaf nodes
|
||||
clay.image = function(path, ...configs) {
|
||||
var img = graphics.texture(path)
|
||||
var c = {image: path}
|
||||
var final_config = process_configs(configs)
|
||||
if (!final_config.size && !final_config.behave) {
|
||||
c.size = {width: img.width, height: img.height}
|
||||
}
|
||||
|
||||
push_node([c, ...configs], null)
|
||||
pop_node()
|
||||
}
|
||||
|
||||
clay.text = function(str, ...configs) {
|
||||
var c = {text: str}
|
||||
var final_config = process_configs(configs)
|
||||
if (!final_config.size && !final_config.behave) {
|
||||
c.size = {width: 100, height: 20}
|
||||
}
|
||||
|
||||
push_node([c, ...configs], null)
|
||||
pop_node()
|
||||
}
|
||||
|
||||
clay.rectangle = function(...configs) {
|
||||
push_node(configs, null)
|
||||
pop_node()
|
||||
}
|
||||
|
||||
clay.button = function(str, action, ...configs) {
|
||||
var btn_config = {
|
||||
padding: 10,
|
||||
background_color: {r:0.3, g:0.3, b:0.4, a:1}
|
||||
}
|
||||
|
||||
clay.zstack([btn_config, ...configs], function() {
|
||||
clay.text(str, {color: {r:1,g:1,b:1,a:1}})
|
||||
})
|
||||
}
|
||||
|
||||
// Constants
|
||||
clay.behave = layout.behave
|
||||
clay.contain = layout.contain
|
||||
|
||||
return clay
|
||||
251
draw2d.cm
251
draw2d.cm
@@ -1,251 +0,0 @@
|
||||
var math = use('math')
|
||||
var color = use('color')
|
||||
var prosperon = use('prosperon')
|
||||
|
||||
var draw = {}
|
||||
|
||||
var current_list = []
|
||||
|
||||
// Clear current list
|
||||
draw.clear = function() {
|
||||
current_list = []
|
||||
}
|
||||
|
||||
// Get commands from current list
|
||||
draw.get_commands = function() {
|
||||
return current_list
|
||||
}
|
||||
|
||||
// Helper to add a command
|
||||
function add_command(type, data) {
|
||||
data.cmd = type
|
||||
current_list.push(data)
|
||||
}
|
||||
|
||||
// Default geometry definitions
|
||||
var ellipse_def = {
|
||||
start: 0,
|
||||
end: 1,
|
||||
mode: 'fill',
|
||||
thickness: 1,
|
||||
}
|
||||
|
||||
var line_def = {
|
||||
thickness: 1,
|
||||
cap:"butt",
|
||||
}
|
||||
|
||||
var rect_def = {
|
||||
thickness:1,
|
||||
radius: 0
|
||||
}
|
||||
|
||||
var image_info = {
|
||||
tile_x: false,
|
||||
tile_y: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
mode: 'linear'
|
||||
}
|
||||
|
||||
var circle_def = {
|
||||
inner_radius:1, // percentage: 1 means filled circle
|
||||
start:0,
|
||||
end: 1,
|
||||
}
|
||||
|
||||
// Drawing functions
|
||||
draw.point = function(pos, size, opt = {}, material) {
|
||||
add_command("draw_point", {
|
||||
pos: pos,
|
||||
size: size,
|
||||
opt: opt,
|
||||
material: material
|
||||
})
|
||||
}
|
||||
|
||||
draw.ellipse = function(pos, radii, defl, material) {
|
||||
var opt = defl ? {...ellipse_def, ...defl} : ellipse_def
|
||||
if (opt.thickness <= 0) opt.thickness = number.max(radii[0], radii[1])
|
||||
|
||||
add_command("draw_ellipse", {
|
||||
pos: pos,
|
||||
radii: radii,
|
||||
opt: opt,
|
||||
material: material
|
||||
})
|
||||
}
|
||||
|
||||
draw.line = function(points, defl, material)
|
||||
{
|
||||
var opt = defl ? {...line_def, ...defl} : line_def
|
||||
|
||||
add_command("draw_line", {
|
||||
points: points,
|
||||
opt: opt,
|
||||
material: material
|
||||
})
|
||||
}
|
||||
|
||||
draw.cross = function render_cross(pos, size, defl, material) {
|
||||
var a = [pos.add([0, size]), pos.add([0, -size])]
|
||||
var b = [pos.add([size, 0]), pos.add([-size, 0])]
|
||||
draw.line(a, defl, material)
|
||||
draw.line(b, defl, material)
|
||||
}
|
||||
|
||||
draw.arrow = function render_arrow(start, end, wingspan = 4, wingangle = 10, defl, material) {
|
||||
var dir = math.norm(end.sub(start))
|
||||
var wing1 = [math.rotate(dir, wingangle).scale(wingspan).add(end), end]
|
||||
var wing2 = [math.rotate(dir, -wingangle).scale(wingspan).add(end), end]
|
||||
draw.line([start, end], defl, material)
|
||||
draw.line(wing1, defl, material)
|
||||
draw.line(wing2, defl, material)
|
||||
}
|
||||
|
||||
draw.rectangle = function render_rectangle(rect, defl, material = {color:{r:1,g:1,b:1,a:1}}) {
|
||||
var opt = defl ? {...rect_def, ...defl} : rect_def
|
||||
|
||||
add_command("draw_rect", {
|
||||
rect,
|
||||
opt,
|
||||
material
|
||||
})
|
||||
}
|
||||
|
||||
var slice9_info = {
|
||||
tile_top:true,
|
||||
tile_bottom:true,
|
||||
tile_left:true,
|
||||
tile_right:true,
|
||||
tile_center_x:true,
|
||||
tile_center_right:true,
|
||||
}
|
||||
|
||||
draw.slice9 = function slice9(image, rect = [0,0], slice = 0, info = slice9_info, material) {
|
||||
if (!image) throw Error('Need an image to render.')
|
||||
|
||||
add_command("draw_slice9", {
|
||||
image,
|
||||
rect,
|
||||
slice,
|
||||
info,
|
||||
material
|
||||
})
|
||||
}
|
||||
|
||||
draw.image = function image(image, rect, scale = {x:1,y:1}, anchor, shear, info, material) {
|
||||
if (!rect) throw Error('Need rectangle to render image.')
|
||||
if (!image) throw Error('Need an image to render.')
|
||||
|
||||
if (!('x' in rect && 'y' in rect)) throw Error('Must provide X and Y for image.')
|
||||
|
||||
add_command("draw_image", {
|
||||
image,
|
||||
rect,
|
||||
scale,
|
||||
anchor,
|
||||
shear,
|
||||
info,
|
||||
material
|
||||
})
|
||||
}
|
||||
|
||||
draw.circle = function render_circle(pos, radius, defl, material) {
|
||||
draw.ellipse(pos, [radius,radius], defl, material)
|
||||
}
|
||||
|
||||
// wrap is the width before wrapping
|
||||
// config is any additional config to pass to the text renderer
|
||||
var text_base_config = {
|
||||
align: 'left', // left, right, center, justify
|
||||
break: 'word', // word, character
|
||||
}
|
||||
draw.text = function text(text, pos, font = 'fonts/c64.8', color = {r:1,g:1,b:1,a:1}, wrap = 0, config = {}) {
|
||||
config.align ??= text_base_config.align
|
||||
config.break ??= text_base_config.break
|
||||
|
||||
add_command("draw_text", {
|
||||
text,
|
||||
pos,
|
||||
font,
|
||||
wrap,
|
||||
material: {color},
|
||||
config
|
||||
})
|
||||
}
|
||||
|
||||
draw.grid = function grid(rect, spacing, thickness = 1, offset = {x: 0, y: 0}, material) {
|
||||
if (!rect || rect.x == null || rect.y == null ||
|
||||
rect.width == null || rect.height == null) {
|
||||
throw Error('Grid requires rect with x, y, width, height')
|
||||
}
|
||||
if (!is_object(spacing)|| is_null(spacing.x) || is_null(spacing.y)) {
|
||||
throw Error('Grid requires spacing with x and y')
|
||||
}
|
||||
|
||||
var left = rect.x
|
||||
var right = rect.x + rect.width
|
||||
var top = rect.y
|
||||
var bottom = rect.y + rect.height
|
||||
|
||||
// Apply offset and align to grid
|
||||
var start_x = number.floor((left - offset.x) / spacing.x) * spacing.x + offset.x
|
||||
var end_x = number.ceiling((right - offset.x) / spacing.x) * spacing.x + offset.x
|
||||
var start_y = number.floor((top - offset.y) / spacing.y) * spacing.y + offset.y
|
||||
var end_y = number.ceiling((bottom - offset.y) / spacing.y) * spacing.y + offset.y
|
||||
|
||||
// Draw vertical lines
|
||||
for (var x = start_x; x <= end_x; x += spacing.x) {
|
||||
if (x >= left && x <= right) {
|
||||
var line_top = number.max(top, start_y)
|
||||
var line_bottom = number.min(bottom, end_y)
|
||||
draw.line([[x, line_top], [x, line_bottom]], {thickness: thickness}, material)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw horizontal lines
|
||||
for (var y = start_y; y <= end_y; y += spacing.y) {
|
||||
if (y >= top && y <= bottom) {
|
||||
var line_left = number.max(left, start_x)
|
||||
var line_right = number.min(right, end_x)
|
||||
draw.line([[line_left, y], [line_right, y]], {thickness: thickness}, material)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw.scissor = function(rect)
|
||||
{
|
||||
var screen_rect = null
|
||||
if (rect && prosperon.camera) {
|
||||
var bottom_left = prosperon.camera.world_to_window(rect.x, rect.y)
|
||||
var top_right = prosperon.camera.world_to_window(rect.x + rect.width, rect.y + rect.height)
|
||||
var screen_left = bottom_left.x
|
||||
var screen_top = bottom_left.y
|
||||
var screen_right = top_right.x
|
||||
var screen_bottom = top_right.y
|
||||
|
||||
screen_rect = {
|
||||
x: number.round(screen_left),
|
||||
y: number.round(screen_top),
|
||||
width: number.round(screen_right - screen_left),
|
||||
height: number.round(screen_bottom - screen_top)
|
||||
}
|
||||
|
||||
// TODO: must be a better way than manually inverting here. Some camera specific function.
|
||||
var sensor = prosperon.camera.sensor()
|
||||
screen_rect.y = sensor.height - screen_rect.y - screen_rect.height
|
||||
}
|
||||
|
||||
current_list.push({
|
||||
cmd: "scissor",
|
||||
rect: screen_rect
|
||||
})
|
||||
}
|
||||
|
||||
draw.add_command = function(cmd)
|
||||
{
|
||||
current_list.push(cmd)
|
||||
}
|
||||
|
||||
return draw
|
||||
Reference in New Issue
Block a user